colorize <- function(x, color) {
if (knitr::is_latex_output()) {
sprintf("\\textcolor{%s}{%s}", color, x)
} else if (knitr::is_html_output()) {
sprintf("<span style='color: %s;'>%s</span>",
color,
x)
} else x
}1 Kiến thức R cơ bản
đây là ký hiệu 鐕
đây là ký hiệu
đây là ký hiệu 🄬
đây là ký hiệu 🅡
đây là ký hiệu 🆁
đây là ký hiệu Ⓡ
đây là ký hiệu ℝ
Mục đích của cuốn sách này không phải để bạn đọc trở thành một lập trình viên chuyên nghiệp. Cuốn sách được viết nhằm giúp bạn đọc có thể sử dụng R và thực hiện được mục đích của mình một cách nhanh nhất. Theo quan điểm của chúng tôi, R không phải là một ngôn ngữ thích hợp để bắt đầu cho học lập trình. Muốn trở thành một lập trình viên giỏi, bạn đọc nên bắt đầu với các ngôn ngữ lập trình cơ bản như Pascal, C++, Java, hay cũng có thể bắt đầu với ngôn ngữ Python.
Cách viết các dòng lệnh của R có thể nói là khá tùy tiện, thậm chí có thể làm cho những người có chuyên môn về lập trình cảm thấy khó chịu. Tuy nhiên, như đã đề cập trong phần giới thiệu của cuốn sách, R có các thế mạnh riêng mà các ngôn ngữ khác không có được và chúng tôi tin rằng R có thể giải quyết được tất cả những yêu cầu của bạn đọc từ những yêu cầu đơn giản đến những yêu cầu phức tạp nhất.
Cuốn sách dành cho cả các bạn đọc chưa từng làm quen với lập trình. Những bạn đọc đã có kinh nghiệm với lập trình có thể bỏ qua các phần không cần thiết.
1.1 Làm quen với các dòng lệnh cơ bản
1.1.1 Sử dụng R như một máy tính cầm tay
Để R hiểu và thực hiện được các yêu cầu của mình, bạn đọc cần phải giao tiếp với R theo ngôn ngữ mà phần mềm này có thể hiểu được. Câu lệnh đầu tiên và đơn giản nhất là hiển thị một giá trị lên màn hình Console. Bạn đọc hãy nhấp chuột vào cửa sổ Console, gõ trực tiếp đoạn câu lệnh như ở dưới và kết thúc câu lệnh bằng cách sử dụng phím Enter.
print("I am MFEer") # In ra màn hình R console dòng chữ I am MFEerBạn đọc có thể bắt đầu làm quen với các dòng lệnh của R bằng cách viết lên cửa sổ Console các công thức để thực hiện tính toán các phép toán dưới đây. R lúc này chỉ đơn giản là một máy tính cầm tay.
1+0.001 # phép tính cộng, số thập phân, số thập phân trong R sử dụng dấu "."
2*pi - 3 # số pi trong R được viết đơn giản là pi; pi nhận giá trị 3.1416...
exp(1)-exp(-1) # exp là hàm số mũ là lũy thừa của số e
log(3.2) # logarit cơ số tự nhiên của số 3.2
log(1000,10) # logarit cơ số 10 của số 1000Bạn đọc có thể tiếp tục thực hành các câu lệnh cơ bản bằng cách tính toán kết quả của các biểu thức dưới đây
\[\begin{align} a) \cfrac{1}{4^{1/6}} \ \ \ b) \cfrac{7 - 4}{12 - 7} \ \ \ c) \sqrt{\cfrac{4}{22}} \ \ \ d) (12-5)^{4/3} \ \ \ e) ln\left( \cfrac{2 + 4}{2^5 -1} \right) \end{align}\]
Khi viết lên cửa sổ Console, R luôn thực hiện câu lệnh mỗi khi bạn đọc sử dụng phím Enter. Để viết hai hay nhiều câu lệnh trên một dòng khi sử dụng cửa sổ Console, bạn đọc hãy kết thúc mỗi câu lệnh bằng dấu “;”. Hãy thử câu lệnh ở dưới và quan sát R sẽ trả kết quả như thế nào
## [1] 1.001
## [1] 3.283185
## [1] 2.350402
Khi bạn đọc viết các câu lệnh đơn giản, sử dụng nhiều câu lệnh trên một dòng có thể hạn chế việc dùng phím Enter nhiều lần, tuy nhiên chúng tôi khuyên bạn đọc khi muốn thực hiện nhiều câu lệnh khác nhau hãy sử dụng cửa sổ Script thay vì viết câu lệnh trực tiếp lên cửa sổ Console. Cách viết câu lệnh trên cửa sổ Script và cho các câu lệnh chạy sẽ được thảo luận ở phần sau.
1.1.2 Sử dụng cửa sổ Script để viết câu lệnh R
Cách tốt nhất bạn đọc nên sử dụng khi viết câu lệnh đó là sử dụng cửa sổ Script. Để mở cửa sổ Script bạn đọc có thể tìm trên thanh công cụ theo trình tự \(File\) \(\rightarrow\) \(New\) \(file\) \(\rightarrow\) \(R\) \(Script\), hoặc bạn đọc sử dụng tổ hợp phím tắt “Ctrl + Shift + N”. Khi viết câu lệnh trên cửa sổ Script, R chỉ thực hiện câu lệnh khi bạn đọc yêu cầu. Do đó, bạn đọc có thể sử dụng cửa sổ Script để viết các chương trình lớn, có nhiều dòng lệnh kế tiếp nhau.
Sau khi mở của sổ Script, bạn đọc có thể viết các dòng lệnh và sử dụng phím Enter để xuống dòng và không cần quan tâm đến việc R có chạy câu lệnh đó hay không. Trong một dòng lệnh trên cửa sổ Script mỗi khi bạn đọc sử dụng dấu ngắt câu lệnh “;” R vẫn hiểu rằng bạn đọc đang viết hai câu lệnh khác nhau trên một dòng:
Để chạy các dòng lệnh trên cửa sổ Script, bạn đọc sử dụng con trỏ và click chuột trái vào nút Run nằm ở phía góc trên bên phải của cửa sổ này hoặc sử dụng tổ hợp phím “Ctrl + Enter”. Để chạy một dòng lệnh riêng lẻ trên Script, bạn đọc di chuyển con trỏ đến dòng lệnh đó và thực hiện thao tác chạy. Để chạy nhiều dòng lệnh trên cửa sổ Script, bạn đọc sử dụng chuột trái lựa chọn các dòng lệnh mình muốn chạy và sau đó thực hiện thao tác chạy. Khi bạn đọc lựa chọn nhiều dòng lệnh một lúc để chạy, R sẽ thực hiện các câu lệnh lần lượt theo thứ tự từ trên xuống dưới và từ bên trái qua bên phải nếu một dòng có nhiều câu lệnh.
Lưu ý, khi bạn đọc viết một chương trình bao gồm nhiều dòng lệnh, bạn thường phải sử dụng ngôn ngữ thông thường như tiếng Việt, tiếng Anh, …, để ghi chú lại các dòng lệnh hoặc nhóm các dòng lệnh đó có ý nghĩa là gì. Việc này giúp cho chính bản thân bạn đọc khi xem lại các dòng lệnh R của mình và những người khác khi đọc các dòng lệnh, có thể hiểu được nhanh hơn bạn đọc đang làm gì. Các câu ghi chú đó theo ngôn ngữ lập trình được gọi là các câu \(comment\). Để R sẽ hiểu được đó là các câu ghi chú bạn đọc cần phải thêm dấu “#” trước các câu đó.
# Đây là cách tính xấp xỉ số e
n<-1000
cat("e = ", (1+1/n)^n) # Khi n càng lớn thì kết quả càng chính xác1.2 Biến trong R
Biến là khái niệm cơ bản nhất trong mọi ngôn ngữ lập trình. Có bốn loại biến cơ bản trong R: biến kiểu số, biến kiểu ký tự, biến kiểu logic, và biến kiểu thời gian. Một số tài liệu khác khi viết về ngôn ngữ lập trình R phân loại biến thành nhiều kiểu hơn, có thêm kiểu số nguyên, kiểu factor,… Theo quan điểm của chúng tôi, phân loại biến quá chi tiết sẽ gây khó khăn cho bạn đọc, nhất là với bạn đọc mới làm quen với lập trình. Trong các phần tiếp theo của cuốn sách, chúng tôi sẽ thảo luận về mỗi kiểu biến cụ thể.
Để tạo một biến trong R và gán giá trị cho biến đó, bạn đọc sử dụng một trong ba cách như sau
# Cách thứ nhất
tenbien <- giatri # dấu "<-" là dấu gán giá trị
# Cách thứ hai
tenbien -> giatri
# Cách thứ ba
tenbien = giatri # dấu "=" cũng được sử dụng để gán giá trịTrong đó \(tenbien\) là tên của biến mà bạn muốn đặt, \(giatri\) là giá trị mà bạn muốn gán cho biến. Ký tự gán giá trị <- được sử dụng trong các phiên bản R đầu tiên. Gán giá trị cho biến sử dụng ký tự -> hiếm khi được dùng. Từ năm 2001 trở đi, dấu = cũng có thể được sử dụng để gán giá trị cho biến. Tuy nhiên dấu = có thể gây nhầm lẫn sau này khi bạn đọc sử dụng song song với ký hiệu == và ký hiệu = trong truyền giá trị cho tham số khi viết hàm số. Trong cuốn sách này, chúng tôi luôn sử dụng <- để gán giá trị cho biến. Các ví dụ về tạo biến và gán giá trị cho biến ở trong các dòng lệnh phía dưới.
# Cách thứ nhất
x <- 3 # tạo một biến tên là x có giá trị bằng 3
# Cách thứ hai
"MFE" -> y # tao một biến tên là y có giá trị bằng đoạn ký tự "MFE"
# Cách thứ ba
z = 1 + 2 # tạo một biến tên là z và nhận giá trị bằng kết quả của phép cộngTrong các câu lệnh ở trên, \(x\), \(y\) hay \(z\) là tên biến. Quy tắc đặt tên biến hay rộng hơn là tên một đối tượng trong R cần tuân theo các quy tắc sau:
Tên biến có thể là tổ hợp của tất cả các chữ cái viết hoa, chữ cái viết thường và các chữ số.
Trong tên biến có thể chứa hai ký tự đặc biệt là “.” và “_“.
Tên biến không được phép bắt đầu bằng số hoặc ký tự “_“.
Không được dùng từ khóa để đặt tên biến.
Để kiểm tra các quy tắc ở trên, bạn đọc có thể chạy các câu lệnh tạo biến dưới đây và xem dòng lệnh nào báo lỗi và dòng lệnh nào không báo lỗi.
x1 <- 3 # biến tên x1 sẽ được tạo với giá trị bằng 3
1x <- 3 # sẽ báo lỗi vì tên biến không được phép bắt đầu bằng số
.x <- 3 # biến tên .x sẽ được tạo với giá trị bằng 3
_x <- 3 # sẽ báo lỗi vì tên biến không được phép bắt đầu bằng sốLưu ý rằng R có phân biệt chữ viết hoa với chữ viết thường trong tên biến. Chúng ta có thể sử dụng \(x\) để đặt tên và sau đó dùng \(X\) để đặt tên cho một biến khác:
x<-3 # tạo một biến tên x nhận giá trị bằng 3
X<-5 # tạo một biến tên X nhận giá trị bằng 5
X-x # hiệu số nhận giá trị bằng 2 do x và X là khác nhauĐể biết danh sách các tên biến và các biến nhận giá trị nào, ngoài việc in giá trị biến lên của sổ Console bạn đọc có thể sử dụng cửa sổ Environment ở góc phía trên bên phải của Rstudio. Để xóa một biến hoặc một đối tượng nào đó có tên trên cửa sổ Environment, bạn đọc sử dụng lệnh rm()
x # Console sẽ in ra giá trị bằng 3
rm(x) # xóa biến x khỏi Rstudio đang chạy
x # sau khi xóa biến x sẽ không còn tồn tại nên R sẽ báo lỗiMột điều cũng cần lưu ý khi đặt tên biến, hay tên bất kỳ một đối tượng nào khác trong R, đó là tên biến không được phép trùng với các từ khóa. Danh sách các từ khóa thường sử dụng trong R nằm trong bảng dưới đây
| Từ khóa | Sử dụng trong ngữ cảnh |
|---|---|
| If, else | Câu lệnh điều kiện |
| for, while, in , repeat | Vòng lặp |
| function | Khai báo hàm số |
| break, next | Điều khiển vòng lặp |
| TRUE, FALSE | Tên các biến logic |
| Inf, -Inf, NaN, NA | Các biến kiểu số dạng đặc biệt |
Chúng ta sẽ thảo luận về từng kiểu biến trong các phần tiếp theo.
1.2.1 Biến kiểu số
Biến kiếu số, hay còn được gọi là kiểu \(numeric\), là các biến nhận giá trị kiểu số thập phân. Để tạo một biến kiểu số, bạn đọc hãy khởi tạo biến bằng cách gán một giá trị kiểu số cho tên biến mà bạn muốn đặt. Đây cũng là cách tạo biến chung trong R.
x <- 5 # do 5 là giá trị kiểu số nên R sẽ hiểu x là biến kiểu sốĐể kiểm tra xem \(x\) có phải là biến kiểu số không, bạn đọc sử dụng hàm is.numeric(). Hàm số này trả lại giá trị là kiểu logic. Giá trị \(TRUE\) cho biết biến được hỏi đúng là kiểu số; giá trị \(FALSE\) cho biết biến được hỏi không phải là kiểu số. Ngoài cách sử dụng hàm is.numeric(), bạn đọc cũng có thể sử dụng hàm class(). Cách sử dụng hai hàm này như sau:
is.numeric(x) # do 5 là giá trị kiểu số nên R trả lời TRUE## [1] TRUE
class(x) # do 5 là giá trị kiểu số nên R sẽ hiểu x là biến kiểu số (numeric)## [1] "numeric"
x<-"abc" # thử với biến x không phải là kiểu số
is.numeric(x) # x không phải giá trị kiểu số nên kết quả là FALSE## [1] FALSE
Trong phép gán cho giá trị của biến \(x\) như ở trên, mặc dù giá trị khởi tạo (số 5) là số nguyên nhưng R vẫn mặc định cho rằng \(x\) là số thập phân. Để tạo một biến kiểu số nguyên trong R, bạn đọc cần phải sử dụng chữ “L” phía sau số nguyên đó. Chữ L là viết tắt cho “Long” nghĩa là số nguyên kiểu \(Long\) trong các ngôn ngữ lập trình cơ bản. Số nguyên kiểu \(Long\) là các số nguyên cần 32 bytes (1 byte là 1 ô chứa số 0 hoặc 1) để lưu và nhận \(2^{32}\) giá trị từ −2,147,483,648 (\(-2^{31}\)) đến 2,147,483,647 (\(2^{31}-1\)). Để tạo biến \(x\) nhận giá trị là số nguyên 5 chúng ta viết như sau:
x<-5L # 5L nghĩa là số nguyên 5, L là viết tắt của Long
class(x) # x là số tự nhiên## [1] "integer"
is.numeric(x) # x không còn là số thập phân, nhưng vẫn là kiểu số## [1] TRUE
Phân biệt số nguyên (integer) và số thập phân (numeric) trong các ngôn ngữ lập trình có ý nghĩa khi bạn đọc cần tiết kiệm bộ nhớ cho chương trình. Trong R, khi sử dụng số thập phân thay cho số nguyên, dung lượng bộ nhớ máy tính sẽ tăng gấp 2 lần. Hình vẽ dưới đây mô tả dung lượng bộ nhớ cần sử dụng cho các véc-tơ chứa các số nguyên và các véc-tơ chứa các số thập phân với độ dài (số lượng phần tử trong véc-tơ) từ 1 đến 100. Không có sự khác biệt về bộ nhớ cho véc-tơ có độ dài dưới 10 nhưng khi véc-tơ có độ dài từ 10 trở lên, véc-tơ kiểu số thập phân cần trung bình khoảng 2 lần bộ nhớ so với véc-tơ kiểu số nguyên.

Các phép tính toán thông thường khi sử dụng với biến kiểu số được liệt kê trong bảng dưới đây
| Ký hiệu | Phép tính |
|---|---|
| + | Phép tính cộng |
| - | Phép tính trừ |
| * | Phép tính nhân |
| / | Phép tính chia |
| ^ | Phép tính lũy thừa |
| exp() | Phép tính lũy thừa cơ số e |
| log() | Phép lấy loga cơ số tự nhiên |
| log(.,a) | Phép lấy loga cơ số a |
| %% | Phép lấy phần dư trong phép chia |
| %/% | Phép lấy phần nguyên của kết quả trong phép chia |
Lưu ý rằng các phép toán \(%%\) và \(%\%\) có thể thực hiện được với cả số kiểu thập phân
6.5 %% 2 # phần dư của phép chia 6.5 cho 2, R sẽ trả kết quả là 1.5## [1] 0.5
6.5 %/% 2 # phần nguyên của kết quả của phép chia 6.5 cho 2## [1] 3
Trong R có cách viết biến kiểu số theo kiểu khoa học và các giá trị số đặc biệt mà bạn đọc cũng nên ghi nhớ:
| Loại số | Ý nghĩa |
|---|---|
| 1.2e+8 | nghĩa là nhân số 1.2 với 10 lũy thừa 8 |
| 1.2e-5 | nghĩa là nhân số 1.2 với 10 lũy thừa -5 |
| Inf | Số dương vô cùng |
| -Inf | Số âm vô cùng |
| NaN | là kết quả của các phép tính không có nghĩa, viết tắt của Not a Number |
Bạn đọc có thể thử tính toán trên các giá trị đặc biệt
1/0 # kêt quả của 1/0 là dương vô cùng (Inf)
(-1)/0 # kêt quả của 1/0 là âm vô cùng (-Inf)
Inf - 10^10 # Trong các phép tính có Inf sẽ dẫn đến kết quả là Inf
1/0 + (-1)/0 # Inf + (-Inf) là không thể xác định được (NaN)
log(-2) # Kết quả của các phép tính không có nghĩa là NaN1.2.2 Biến kiểu logic
Biến kiểu logic là kiểu biến đơn giản nhất nhưng lại là kiểu biến quan trọng nhất trong mọi ngôn ngữ lập trình. Biến kiểu logic chỉ nhận một trong hai giá trị là \(TRUE\) hoặc \(FALSE\). Do R phân biệt chữ viết hoa và chữ viết thường nên bạn đọc lưu ý khi viết giá trị cho biến kiểu logic là hoàn toàn các chữ cái viết hoa. Để tạo một biến kiểu logic, bạn đọc tạo đặt tên biến và gán một trong hai giá trị logic cho biến đó. Việc này hoàn toàn giống như khi tạo một biến kiểu số
x<-TRUEBiến kiểu logic có thể đặt trong các phép tính toán giống như biến kiểu số. Khi gặp một công thức có bao gồm cả biến kiểu số và biến kiểu logic, R sẽ đổi biến kiểu logic nhận giá trị \(TRUE\) thành số 1 và biến kiểu logic có giá trị \(FALSE\) thành số 0 để thực hiện phép tính toán.
FALSE + TRUE * 10 # Sẽ cho kết quả giống như 0 + 1 * 10## [1] 10
Trong thực tế, ít khi chúng ta cần phải khởi tạo giá trị cho biến kiểu logic như trên, mà biến kiểu logic thường nhận được từ kết quả các phép so sánh trong R. Các phép toán so sánh này được liệt kê trong bảng dưới đây
| Phép so sánh | Ý nghĩa |
|---|---|
| < | Có nhỏ hơn không? |
| > | Có lớn hơn không? |
| <= | Có nhỏ hơn hoặc bằng không? |
| >= | Có lớn hơn hoặc bằng không? |
| == | Có bằng nhau không? |
| != | Có khác nhau không? |
Ngoài ra, các biến kiểu logic còn là kết quả của việc kết hợp nhiều biến kiểu logic khác bằng các toán tử logic. Các toán tử logic bao gồm có “Và”, “Hoặc” và toán tử “Phủ định”
| Toán tử logic | Ý nghĩa |
|---|---|
| & | Toán tử Và; A&B đọc là A và B |
Bạn đọc cần ghi nhớ quy tắc kết hợp các biến kiểu logic bằng các toán tử logic như bảng dưới đây
| Kết hợp | Kết quả |
|---|---|
| !TRUE | FALSE |
| !FASLE | TRUE |
| TRUE & TRUE | TRUE |
| TRUE & FALSE | FALSE |
| FALSE & TRUE | FALSE |
| TRUE | TRUE | TRUE |
| TRUE | FALSE | TRUE |
| FALSE | TRUE | TRUE |
Như chúng tôi đã đề cập ở phần trên, các biến kiểu logic khi đặt trong các biểu thức tính toán sẽ được tự động đổi sang biến kiểu số trước khi thực hiện phép tính. Ngược lại, khi biến kiểu số xuất hiện trong các biểu thức có toán tử logic, biến kiểu số cũng sẽ được chuyển sang kiểu logic. Tuy nhiên, bạn đọc lưu ý rằng: chỉ có số 0 khi đặt trong biểu thức có toán tử logic mới được chuyển thành \(FALSE\), mọi số khác 0 khi đổi sang kiểu logic đều được chuyển thành \(TRUE\)”
Bạn đọc có thể thực hành việc tính toán trên các toán tử logic như dưới đây. Trước khi sử dụng R để xem kết quả, hãy thử suy nghĩ xem các biểu thức sau đây cho kết quả như thế nào.
# 1.
(1<=2) | (2<=3)
# 2.
(1<=2) + (2<=3)
# 3.
((1<=2) * (2^2 == 4)) | (2!=3) #
# 4.
!((1<=2) * (2^2 == 4)) & !(2!=3) #
# 5.
((2 + 2) | (2 - 2)) & !(2 ^ 2) #1.2.3 Biến kiểu chuỗi ký tự
Trong R, biến kiểu chuỗi ký tự được gọi là kiểu character. Biến kiểu chuỗi ký tự tương tự như biến kiểu xâu ký tự (thường được gọi là string) trong các ngôn ngữ lập trình cơ bản. Biến kiểu chuỗi ký tự có thể chỉ ngắn gọn là một ký tự trống, một chữ cái, đôi khi có thể là cả một câu văn, và cũng có thể là cả một đoạn văn bản dài. Khi làm việc với biến kiểu ký tự, bạn đọc hãy luôn ghi nhớ rằng R phân biệt chữ viết hoa và chữ viết thường.
Để tạo một biến có kiểu ký tự trong R, bạn đọc cần tạo tên biến và gán cho biến giá trị kiểu chuỗi ký tự. R sẽ hiểu một biến là chuỗi ký tự khi chuỗi ký tự đó nằm trong dấu ngoặc kép “” hoặc trong dấu ngoặc đơn (’’).
x<-"Ice cream" # "Ice cream" với chữ I viết hoa sẽ khác "ice cream" khi i là chữ thường
x == "ice cream" # sẽ trả ra giá trị là FALSE## [1] FALSE
Để biết một biến có phải kiểu chuỗi ký tự không, bạn đọc có thể dùng hàm is.character() hoặc hàm class()
is.character(x)## [1] TRUE
class(x)## [1] "character"
Khi xử lý biến kiểu chuỗi ký tự, bạn đọc nên sử dụng các hàm số đã được xây dựng sẵn. Bảng dưới đây liệt kê danh sách các hàm thường sử dụng và kết quả đầu ra của các hàm này
| Hàm số | Ý nghĩa |
|---|---|
| nchar(x) | Cho biêt biến x dạng chuỗi ký tự có bao nhiêu ký tự |
| paste(x1,x2,sep = a) | Ghép hai chuỗi ký tự x1 và x2 thành một chuỗi ký tự cách nhau chuỗi ký tự a |
| toupper(x) | Chuyển tất cả các chữ viêt thường trong x thành chữ viết hoa |
| tolower(x) | Chuyển tất cả các chữ viết hoa trong x thành chữ viết thường |
| chartr(a,b,x) | Thay thế trong x: từng ký tự trong chuỗi a tương ứng bằng từng ký tự trong chuỗi b, a và b phải có độ dài bằng nhau |
| substr(x,k,n) | Lấy ra chuỗi ký tự con từ x, lấy từ ký tự thứ k đến ký tự thứ n |
| sub(a, b, x) | Đoạn ký tự a đầu tiên trong x sẽ được thay thế bằng đoạn ký tự b |
| gsub(a, b, x) | Tất cả các đoạn ký tự giống a trong x sẽ được thay thế bằng b |
| grepl(a,x) | Trả lại giá trị là biến TRUE nếu đoạn ký tự a nằm trong biến x |
Bạn đọc có thể thử các hàm liệt kê trong bảng ở trên và quan sát giá trị trả ra của các hàm để hiểu cách áp dụng:
x1<-"I am an Actuary"; x2<-"I am Vietnamese"
nchar(x1) # cho biết x1 có bao nhiêu ký tự, tính cả các khoảng trống
paste(x1, x2, sep = " and ") # ghép x1 và x2 lại với nhau và thêm " and " vào giữa
toupper(x1); tolower(x1) # chuyển tất cả các ký tụ sang viết hoa/viết thường
chartr("an","bm",x1) # thay tất cả các chữ "a" trong x1 bằng "b" và "n" bằng "m"
substr(x1, 9, 15) # lấy ra đoạn ký tự từ ký tự thứ 9 (chữ A) đến ký tự thứ 15 (chữ "y")
sub("a", "XYZ", x1) # thay chữ "a" đầu tiên trong x1 bằng đoạn "XYZ"
gsub("a", "XYZ", x1) # thay tất cả chữ "a" trong x1 bằng đoạn "XYZ"
grepl("Vietnam", x2) # cho biết đoạn ký tự "Vietnam" có nằm trong x2 hay không Nhìn chung xử lý biến kiểu chuỗi ký tự sẽ khó khăn hơn so với xử lý biến kiểu số. Để thực hiện được các yêu cầu phức tạp hơn, bạn đọc có thể kết hợp các hàm số ở trên để có hiệu quả tốt hơn, hoặc sử dụng các thư viện được phát triển dành riêng cho biến kiểu chuỗi ký tự. Chúng tôi thường sử dụng thư viện \(stringr\) khi xử lý biến kiểu chuỗi ký tự. Các hàm hữu ích trong thư viện \(stringr\) sẽ được thảo luận khi chúng ta làm việc với dữ liệu chứa các biến kiểu chuỗi ký tự.
Một kiểu biến bạn đọc cũng thường gặp khi làm việc với dữ liệu trong R là biến hay véc-tơ kiểu factor. Biến kiểu factor cũng có thể được hiểu là biến kiểu chuỗi ký tự nhưng được R lưu trữ dưới dạng tiết kiệm bộ nhớ. Chúng ta sẽ thảo luận kỹ hơn về biến kiểu factor khi làm việc với véc-tơ kiểu chuỗi ký tự.
1.2.4 Biến kiểu thời gian
Trong R có hai kiểu biến thời gian là biến kiểu ngày tháng (\(Date\)) và biến kiểu thời gian chi tiết (\(POSIXct\)). Thời gian POSIX hay còn được biết đến với tên gọi là thời gian Unix là một cách quy ước về thời gian của một thời điểm cụ thể được tính bằng số giây từ cột mốc thời gian Unix đến thời điểm đó. Cột mốc thời gian Unix được các kỹ sư xây dựng hệ điều hành Unix lựa chọn là thời điểm 0 giờ, 0 phút, 0 giây, ngày 01 tháng 01 năm 1970 theo giờ phối hợp quốc tế (giờ UTC). Chữ “ct” là viết tắt của canlendar time. Bạn đọc cũng có thể gặp biến kiểu thời gian chi tiết trong R dưới dạng \(POSIXlt\) trong đó “lt” là chữ viết tắt của local time. Sự khác biệt của biến kiểu \(POSIXct\) và \(POSIXlt\) chỉ là cách R lưu trữ các biến này dưới dạng số nguyên hay dưới dạng véc-tơ. Trong cuốn sách này khi nói đến biến kiểu thời gian chúng tôi luôn sử dụng biến kiểu \(POSIXct\).
Để tạo một biến kiểu thời gian trong R, bạn đọc sử dụng hàm as.Date() cho biến kiểu ngày tháng và hàm as.POSIXct() cho biến kiểu thời gian chi tiết:
date1<-as.Date("2023-08-31") # biến date1 nhận giá trị là ngày 31 tháng 08 năm 2023
time1<-as.POSIXct("2023-08-31 16:41:30") # biến time1 là 16 giờ, 41 phút, 30 giây ngày 31 tháng 08 năm 2023Khi xử lý biến kiểu thời gian, bạn đọc nên đổi sang dạng số hoặc lưu biến kiểu thời gian dưới dạng một véc-tơ số lưu lại các thành phần của thời gian theo một thứ tự nhất định. Hàm as.numeric() sẽ đổi các biến kiểu ngày tháng hoặc thời gian chi tiết ra thành số ngày (đối với biến kiểu ngày tháng) hoặc số giây (đối với biến kiểu thời gian chi tiết) tính từ mốc thời gian Unix.
as.numeric(date1) # cho biết số ngày tính từ 01/01/1970 đến date1## [1] 19600
time2<-as.POSIXct("1970-01-01 07:00:30")
as.numeric(time2) # cho biết số giây tính từ 7 giờ, 0 phút, 0 giây ngày 01/01/1970 đến time2## [1] 30
Do múi giờ UTC của Việt Nam là \(UTC + 7\) nên thời điểm tính làm mốc sẽ là 7 giờ, 0 phút, 0 giây ngày 01 tháng 01 năm 1970. Điều này giải thích tại sao khi đổi biến time2 thành dạng số ta sẽ thu được kết quả là 30 giây. Khi sử dụng các hàm as.Date() hoặc as.POSIXct() giá trị được đưa vào phải là biến dạng chuỗi ký tự được viết theo đúng quy tắc “YYYY-MM-DD” và “YYYY-MM-DD hh:mm:ss”. Trong trường hợp chuỗi ký tự được đưa vào không đúng định dạng, bạn đọc cần phải thông báo cho R biết định dạng của biến chuỗi ký tự đó bằng cách sử dụng thêm tùy biến \(format\). Bạn đọc có thể tham khảo cách khai báo định dạng của biến chuỗi ký tự trong các hàm as.Date hoặc as.POSIXct() như sau
date1<-as.Date("02/27/92", format = "%m/%d/%y") # date1 sẽ nhận giá trị là ngày 27 tháng 02 năm 1992
date2<-as.Date("02 Jan 2010", format = "%d %b %Y") # ngày 02 tháng 01 năm 2010Trong rất nhiều trường hợp, biến kiểu thời gian sẽ được lấy từ các nguồn khác nhau vào R và được lưu dưới dạng số tự nhiên. Điển hình là khi bạn đọc lấy dữ liệu từ các file được lưu từ phần mềm Microsoft Excel. Các hàm as.Date() và as.POSIXct() cũng có thể chuyển giá trị số biến kiểu ngày tháng và biến kiểu thời gian chi tiết. Bạn đọc cần sử dụng thêm tùy biến \(origin\) trong các hàm này để quy định mốc thời gian.
date1<-as.Date(19000, origin = "1970-01-01")
time1<-as.POSIXct(10^9, origin = "1970-01-01 07:00:00")Sau khi chạy các câu lệnh ở trên, biến \(date1\) tương ứng với ngày thứ 19000 tính từ mốc ngày 1 tháng 1 năm 1970 và biến \(time1\) tương ứng với thời điểm giây thứ 1 tỷ tính từ 07 giờ (đúng) ngày 1 tháng 1 năm 1970.
Vấn đề thường gặp phải đó là cách chuyển đổi từ thời gian thành số của phần mềm lưu dữ liệu gốc có mốc thời gian khác với R. Chẳng hạn như biến kiểu thời gian từ Microsoft Excel khi chuyển đổi thành số sử dụng mốc thời gian là ngày 30 tháng 12 năm 1899. Giả sử khi bạn đọc lấy một biến thời gian từ Microsoft Excel vào R và thấy giá trị là 45.678. Nếu không sử dụng mốc thời gian của Microsoft Excel để chuyển đổi, giá trị thời gian nhận được sẽ không đúng.
date1<-as.Date(45678, origin = "1970-01-01")
date1 # date1 sẽ nhận giá trị SAI khi nhận định mốc thời gian là ngày 01 tháng 01 năm 1970## [1] "2095-01-23"
date2<-as.Date(45678, origin = "1899-12-30")
date2 # date2 sẽ nhận giá trị ĐÚNG do khi chuyển đổi đã dùng đúng mốc thời gian của Excel## [1] "2025-01-21"
Nguyên tắc cơ bản khi xử lý và tính toán với biến kiểu thời gian trong R là luôn luôn đổi biến sang kiểu số nguyên hoặc đổi một biến kiểu thời gian thành một véc-tơ chứa các thành phần ngày, tháng, năm, giờ, phút, giây ở dạng số. Để tách biến kiểu ngày tháng ra thành ngày, tháng, năm bạn đọc có thể sử dụng hàm sub.str() để lấy ra các đoạn ký tự chứa giá trị ngày, tháng, và năm rồi sau đó sử dụng hàm as.numeric() để đổi các biến thành biến kiểu số:
year<-as.numeric(substr(date2,1,4)) # sẽ lấy ra đoạn ký tự từ 1-4 trong date2 sau đó đổi đoạn ký tự thành số
month<-as.numeric(substr(date2,6,7)) # sẽ lấy ra đoạn ký tự từ 6-7 trong date2 sau đó đổi đoạn ký tự thành số
day<-as.numeric(substr(date2,9,10)) # sẽ lấy ra đoạn ký tự từ 9-10 trong date2 sau đó đổi đoạn ký tự thành sốXử lý biến kiểu ngày tháng và biến kiểu thời gian phức tạp hơn so với xử lý biên kiểu số và thường cần thêm các thư viện bổ sung. Thư viện thường chúng tôi được sử dụng khi làm việc với biến kiểu thời gian là thư viện \(lubridate\) và thư viện \(hms\). Bạn đọc sẽ sử dụng các thư viện này để thực hành với biến kiểu thời gian trong chương phân tích dữ liệu.
1.3 Véc-tơ trong R
Trong phần này của cuốn sách chúng tôi sẽ giới thiệu các khái niệm cơ bản về véc-tơ để bạn đọc có hiểu biết cơ bản nhất về khái niệm của véc-tơ và thế mạnh của R khi làm việc với véc-tơ. Trong tất cả các phần tiếp theo của cuốn sách đều có liên quan đến đối tượng véc-tơ do đó đi quá sâu vào chi tiết trong phần này là không thực sự cần thiết.
1.3.1 Tại sao xử lý véc-tơ là thế mạnh của R?
Véc-tơ là một tập hợp các phần tử có cùng kiểu được sắp xếp theo một thứ tự nhất định. Thứ tự của một phần tử trong véc-tơ thường được gọi là chỉ số. Phần tử đầu tiên trong một véc-tơ của R có chỉ số là 1. Bạn đọc hãy lưu ý điều này bởi trong một vài ngôn ngữ khác chỉ số của phần tử đầu tiên trong véc-tơ sẽ là 0. Vec-tơ là đối tượng quan trọng nhất trong R và xử lý vec-tơ chính là một thế mạnh của R mà đa số các ngôn ngữ cơ bản khác không đáp ứng được.
Khi bạn đọc làm việc với dữ liệu, các thao tác biến đổi dữ liệu thường sẽ là biến đổi đồng thời các giá trị trên cùng một hàng hoặc một cột dữ liệu. Hiếm khi các thao tác này được thực hiện với một giá trị riêng lẻ. Đối tượng véc-tơ là một công cụ hiệu quả để thực hiện các công việc này. Hiệu quả ở đây không chỉ bao gồm sự tiện lợi khi viết các câu lệnh, mà còn hiệu quả ở cả thời gian thực hiện tính toán. Trong phần Lập trình với R chúng tôi sẽ thảo luận kỹ hơn về hiệu quả về thời gian tính toán. Hãy nói về sự tiện lợi khi sử dụng véc-tơ trước. Chúng tôi thực hiện một phân tích trên dữ liệu có tên là \(trump\_tweets\) nằm trong thư viện \(dslabs\) bằng cách chạy một đoạn lệnh sau
library(dslabs) # cần gọi thư viện dslabs chứa dữ liệu trump_tweets
barplot(table(as.factor(as.numeric(substr(trump_tweets$created_at,12,13)))),
main = "Tổng thống Trump viết tweet vào thời gian nào trong ngày", col = "lightskyblue")
Dữ liệu \(trump\_tweets\) là dữ liệu chứa hơn 20 nghìn câu “tweets” của cựu tổng thống Mỹ Donald Trump trong khoang thời gian từ 2009 đến 2017. Đoạn câu lệnh trên thực hiện một phân tích cho biết kết quả là Donald Trump có thói quen viết “tweets” vào thời gian nào trong ngày. Kết quả này thu được bằng việc thực hiện 1 loạt các phép biến đổi và tính toán cột có tên là \(created\_at\) của dữ liệu:
- Lấy ra đoạn ký tự chứa giá trị là giờ của cột \(created\_at\) (dùng hàm
substr()). - Chuyển đổi dữ liệu kiểu chuỗi ký sang kiểu số (dùng hàm
as.numeric()). - Chuyển đổi dữ liệu kiểu số sang kiểu factor (dùng hàm
as.factor()) - Tổng hợp lại dữ liệu kiểu factor theo các nhóm (dùng hàm
table()) - Vẽ đồ thị kiểu \(barplot\) để người đọc hiểu về dữ liệu một cách nhanh chóng và trực quan hơn.
Để đi từ cột dữ liệu \(created\_at\) kiểu \(POSIXct\) đến kết quả là đồ thị dạng \(barplot\) mà chỉ cần một dòng lệnh là việc gần như không thể đối với đa số các ngôn ngữ lập trình. Các ngôn ngữ lập trình cơ bản chỉ cho phép người sử dụng tác động đển từng phần tử của véc-tơ một cách lần lượt và riêng lẻ. Trái lại, khi bạn đọc thực hiện một phép biến đổi hay tính toán trên đối tượng là véc-tơ trong R, các phép tính toán hay biến đổi này sẽ được thực hiện một cách đồng thời cho tất cả các phần tử trong véc-tơ. Ngoài việc giúp cho các câu lệnh trở nên đơn giản, dể hiểu, R cũng được phát triển để những tính toán trên véc-tơ được thực hiện theo cơ chế song song. Cơ chế song song hiểu một cách đơn giản là việc thực hiện các phép toán trên các phần tử của một véc-tơ sẽ diễn ra cùng một lúc chứ không thực hiện một cách lần lượt.
Hầu hết các hàm số trên R đều được phát triển theo cơ chế lập trình vec-tơ. Nghĩa là các hàm số được dùng cho một biến kiểu số đều có thể áp dụng được cho một véc-tơ kiểu số hay các hàm số được dùng cho một biến kiểu chuỗi ký tự đều có thể áp dụng được cho một véc-tơ kiểu chuỗi ký tự. Trong ví dụ với cột (véc-tơ) \(created\_at\) của dữ liệu \(trump\_tweets\) ở trên, các hàm số được sử dụng như substr(), as.numeric(), … đều có đầu vào là một véc-tơ và trả lại giá trị là một véc-tơ có độ dài tương ứng.
Ngoài việc thực hiện tính toán trên các véc-tơ riêng lẻ, cơ chế hoạt động của R cũng cho phép thực hiện tính toán tương tác giữa các véc-tơ với nhau. Tương tác giữa hai hay nhiều véc-tơ với nhau luôn được thực hiện trên nguyên tắc các phần tử có cùng chỉ số của các véc-tơ sẽ tương tác với nhau. Thậm chí các véc-tơ tương tác với nhau có thể không có cùng kích thước mà vẫn cho kết quả. Chi tiết sẽ được thảo luận trong các phần tiếp theo.
1.3.2 Khởi tạo véc-tơ và các phép toán trên véc-tơ.
1.3.2.1 Khởi tạo véc-tơ.
Để khởi tạo một vec-tơ trong R bạn đọc có thể sử dụng bất kỳ một hàm số sẵn có với đầu ra là một véc-tơ với kiểu giá trị phù hợp. Hàm số thông dụng nhất được dùng để tạo véc-tơ trong R là hàm c(); \(c\) là viết tắt của concatenate, hoặc một vài tài liệu cho rằng \(c\) là viết tắt của combine. Về mặt ý nghĩa, hàm c() tập hợp các đối tượng được liệt kê trong dấu \(()\) lại để tạo thành một véc-tơ đối tượng duy nhất. Nếu các phần tử được liệt kê ra có cùng kiểu dữ liệu, đối tượng tượng tạo thành sẽ là một véc-tơ
x<-c(1,1,2,3,5,8,13,21) # x là một vec-tơ kiểu số
qua = c("chuối", "táo", "cam", "chanh") # qua là vec-tơ chứa tên các loại quảKhi các biến được liệt kê bên trong hàm c() không cùng kiểu, R sẽ cố gắng phân tích các giá trị đó để đưa ra kết quả phù hợp. Nguyên tắc chung là nếu các giá trị được liệt kê bên trong hàm c() là kiểu số, kiểu logic, hoặc kiểu thời gian thì véc-tơ được tạo thành sẽ là véc-tơ kiểu số. Trong trường hợp có 1 biến được liệt kê ra là kiểu chuỗi ký tự, véc-tơ được tạo thành sẽ là véc-tơ kiểu chuỗi ký tự. Bạn đọc có thể kiểm tra giá trị của các véc-tơ sau:
## [1] "numeric"
## [1] "numeric"
## [1] "character"
Các giá trị bên trong hàm c() cũng có thể là một véc-tơ khác, thậm chí có thể là một ma trận (matrix), hoặc là một đối tượng kiểu mảng (array). Giá trị đầu ra của hàm c() luôn luôn là một véc-tơ. Nếu là ma trận hoặc mảng hàm c() sẽ “duỗi” các phần tử ra thành 1 véc-tơ theo thứ tự các cột bắt đầu từ cột có chỉ số 1. Chúng ta sẽ quay lại vấn đề này khi thảo luận về ma trận và mảng.
x<-c(1, TRUE, as.Date("2023-12-31"),"MFE") # kết quả là một véc-tơ kiểu chuỗi ký tự
y<-c(x,"Actuary",x) # dùng véc-tơ x trong khai báo véc-tơ yBất kỳ hàm số sẵn có nào có đầu ra là véc-tơ đều có thể dùng để tạo thành véc-tơ. Các hàm mà chúng tôi hay sử dụng để khởi tạo véc-tơ trong R ngoài hàm c() còn có hàm rep() và hàm seq(). Hàm số rep(x,n) có ý nghĩa là lặp lại giá trị \(x\) (1 biến hoặc 1 véc-tơ) \(n\) lần. Hàm số seq(from = a, to = b,length = n) tạo thành một dãy số tăng dần (hoặc giảm dần) bắt đầu từ \(a\) kết thúc tại \(b\) và véc-tơ có độ dài là \(n\).
x<-rep(1,10^3) # Véc-tơ có các giá trị đều là 1, độ dài 1.000
y<-rep(c("a","b"),10^3) # Lặp lại véc-tơ ("a","b") 1.000 lần
z<-seq(from = 0,to = 1,length = 101) # Dãy số tăng dần từ 0 đến 1, độ dài là 101Đầu ra của seq() luôn là một véc-tơ kiểu số. Nếu bạn đọc không sử dụng tùy biến \(length = n\), bạn đọc có thể sử dụng tùy biến là khoảng cách giữa hai số liên tiếp trong dãy số.
1.3.2.2 Các hàm số thường sử dụng trên véc-tơ
| Hàm số | Ý nghĩa | Áp dụng trên |
|---|---|---|
| length(x) | Số lượng phần tử trong véc-tơ \(x\) | Mọi kiểu véc-tơ |
| sum(x) | Tổng các số trong véc-tơ \(x\) | Kiểu số, logic, thời gian |
| prod(x) | Tích các số trong véc-tơ \(x\) | Kiểu số, logic, thời gian |
| mean() | Giá trị trung bình của các số trong véc-tơ \(x\) | Kiểu số, logic, thời gian |
| var(x) | Phương sai của các giá trị trong véc-tơ \(x\) | Kiểu số, logic, thời gian |
| sd(x) | Độ lệch chuẩn của các giá trị trong véc-tơ \(x\) | Kiểu số, logic, thời gian |
| min(x) | Giá trị nhỏ nhất trong \(x\) | Mọi kiểu véc-tơ |
| max(x) | Giá trị lớn nhất trong \(x\) | Mọi kiểu véc-tơ |
| quantile(x,p) | Giá trị tại mức xác suất \(p\) của véc-tơ \(x\) | Kiểu số, logic, thời gian |
| sort(x) | Sắp xếp các phần tử của \(x\) theo thứ tự TĂNG dần | Mọi kiểu véc-tơ |
| table(x) | Cho biết tần suất xuất hiện của mỗi phần tử | Mọi kiểu véc-tơ |
Bạn đọc lưu ý rằng còn nhiều hàm số hữu ích khác được xây dựng sẵn khi tính toán với véc-tơ mà chúng tôi không liệt kê ở đây. Đồng thời, mỗi hàm số còn có các tùy biến đề sử dụng trong các hoàn cảnh khác nhau. Chẳng hạn khi trong véc-tơ \(x\) có giá trị \(NaN\) hoặc \(NA\) thì các hàm như \(sum(x)\), \(mean(x)\), … sẽ trả lại giá trị là \(NA\). Trong trường hợp này, bạn đọc cần sử dụng thêm tùy biến \(na.rm=TRUE\) để R hiểu rằng các phép tính toán chỉ thực hiện trên các giá trị có ý nghĩa.
## [1] NA
sum(x,na.rm=TRUE) # sẽ trả lại giá trị là $NA$ vì trong $x$ có giá trị $NA$## [1] 15
Cách tốt nhất để hiểu và sử dụng hiệu quả và đúng mục đích các hàm số liệt kê ở trên là đọc hướng dẫn của hàm số đó. Trong cuốn sách này chúng tôi chỉ nhấn mạnh những ứng dụng mà chúng tôi cho rằng quan trọng khi ứng dụng các hàm số trong thực tế.
Các hàm số sử dụng trên các véc-tơ kiểu số như \(sum()\), \(mean()\), hay thậm chí cả \(var()\), \(sd()\) có thể hoạt động trên cả véc-tơ kiểu thời gian hoặc kiểu logic. Nếu phép toán thực hiện không thể giữ nguyên kiểu dữ liệu của véc-tơ thì R sẽ đổi véc-tơ kiểu thời gian hoặc logic sang kiểu số để thực hiện tính toán.
## [1] "2023-07-02"
sd(x) # kiểu thời gian ko có ý nghĩa nên R sẽ đổi x sang kiểu số để tính toán## [1] 257.3869
Ngoài các nguyên tắc tính toán thông thường, bạn đọc thấy rằng R có thể sắp xếp các phần tử trong một véc-tơ bất kỳ bằng hàm sort() hoặc có thể lấy ra giá trị “lớn nhất” hoặc “nhỏ nhất” của một véc-tơ đó bằng hàm max() hoặc hàm min(). Điều này là khá hiển nhiên với các véc-tơ kiểu số. Trong trường hợp véc-tơ là véc-tơ kiểu logic hay kiểu ngày tháng, R sẽ đổi giá trị của véc-tơ đó sang kiểu số để tiến hành sắp xếp hay tìm ra giá trị lớn nhất, giá trị nhỏ nhất. Chắc hẳn bạn đọc sẽ đặt câu hỏi về cách sắp xếp các phần tử trong véc-tơ kiểu chuỗi ký tự. Đây là một vấn đề phức tạp liên quan đến việc mã hóa các ký tự trên máy tính và vượt quá phạm vi của cuốn sách. Bạn đọc chỉ cần ghi nhớ các nguyên tắc sau khi sắp xếp véc-tơ kiểu chuỗi ký tự:
Nếu véc-tơ kiểu chuỗi ký tự được biến đổi thành kiểu factor thì thứ tự sắp xếp tăng dần sẽ phụ thuộc vào cách định nghĩa các mức độ (level) của véc-tơ kiểu factor.
Khi so sánh hai chuỗi ký tự, phép so sánh sẽ được thực hiện ở ký tự thứ nhất trước, nếu hai ký tự đầu tiên giống nhau thì sẽ so sánh ký tự tiếp theo, và tiếp tục như thế đến khi có sự khác biệt.
Các ký tự đặc biệt luôn được xếp trước (nhỏ hơn), sau đó đến các ký tự là các số, rồi đến chữ cái. Thứ tự sắp xếp của các ký tự số theo đúng thứ tự tăng dần từ 0 đến 9 trong khi thứ tự sắp xếp của các chữ cái là tăng dần theo bảng chữ cái. Chữ viết thường được viết trước (nhỏ hơn) chữ viết hoa của chữ cái đó. Chữ viết hoa của chữ cái đứng trước lại “nhỏ hơn” chữ viết thường của chữ đứng sau trong bảng chữ cái.
Trước khi sử dụng R để in ra kết quả, bạn đọc hãy thử “đoán” xem R sẽ trả lại kết quả như thế nào khi chạy các câu sắp xếp các véc-tơ sau theo thứ tự TĂNG dần:
## [1] "a" "az" "z"
## [1] "a" "A" "az" "z" "Z"
## [1] "1a" "a" "A" "az" "z" "Z"
## [1] "@a" "1a" "a" "A" "az" "z" "Z"
## [1] "@a" "0123" "1a" "a" "A" "az" "z" "Z"
Hàm sort() nếu không sử dụng thêm tham số sẽ luôn sắp xếp véc-tơ theo thứ tự tăng dần. Để sắp xếp véc-tơ theo thứ tự giảm dần, bạn đọc có thể sử dụng thêm tùy biến \(decreasing = TRUE\) hoặc ngắn gọn hơn là \(decreasing = T\) trong hàm sort().
## [1] 21 13 8 5 3 2 1 1
## [1] "Z" "z" "az" "A" "a" "1a" "0123" "@a"
1.3.2.3 Tính toán trên véc-tơ
Như đã đề cập ở phần trước, R là ngôn ngữ lập trình véc-tơ. Bạn đọc có thể sử dụng véc-tơ như một đối tượng trong các phép tính toán hoặc so sánh mà không cần phải tác động đến từng phần tử riêng lẻ của véc-tơ đó. Điều này là không thể thực hiện được với các ngôn ngữ lập trình cơ bản.
Trước hết, chúng ta có thể đưa một véc-tơ \(x\) kiểu số vào trong các phép tính toán thông thường như cộng, trừ, nhân, chia, lũy thừa, … với các số thực. Kết quả thu được sẽ là một véc-tơ có độ dài bằng với véc-tơ ban đầu:
x<-1:5 # tạo thành véc-tơ dãy số tự nhiên từ 1 đến 5
x * 2 # nhân véc-tơ với một số## [1] 2 4 6 8 10
x ^ 2 # phép lũy thừa, đối tượng là## [1] 1 4 9 16 25
x %% 2 # lấy phần dư trong phép chia cho 2## [1] 1 0 1 0 1
Quan sát kết quả được in ra, bạn đọc có thể nhận thấy rằng nguyên tắc thực hiện phép tính véc-tơ \(x\) nhân với số 2, hay bất kỳ phép tính nào khác, là lấy các phần tử riêng lẻ trong véc-tơ \(x\) nhân lên 2 và lưu lại trong một véc-tơ mới. Tương tự như phép tính toán, phép so sánh cũng có thể thực hiện giữa một véc-tơ với biến riêng lẻ để cho kết quả là một véc-tơ của các biến logic.
x<-c(1,1,2,3,5,8,13,21) # véc-tơ x kiểu số
x == 1 # Trả lại giá trị TRUE tại các vị trí bằng 1.## [1] TRUE TRUE FALSE FALSE FALSE FALSE FALSE FALSE
(x > 10) | (x < 3) # trả lại giá trị TRUE tại các vị trí lớn hơn 10 hoặc nhỏ hơn 3## [1] TRUE TRUE TRUE FALSE FALSE FALSE TRUE TRUE
s<-c("a","az","z","A","Z","1a","@a", "0123")
s == "a" # Trả lại giá trị TRUE tại các vị trí bằng "a"## [1] TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
Hầu hết các hàm số sẵn có trong R, hoặc các hàm số được phát triển trong các thư viện của R, đều có thể áp dụng trên đối tượng là véc-tơ và nguyên tắc áp dụng hàm số trên véc-tơ cũng tương tự như nguyên tắc tính toán giữa véc-tơ với một số. Việc thực hiện tính toán sẽ được thực hiện trên các phần tử riêng lẻ của véc-tơ và sau đó lưu lại trong một véc-tơ mới có chiều dài bằng với véc-tơ ban đầu. Ví dụ như hàm nchar() cho biết một biến kiểu chuỗi ký tự có bao nhiêu ký tự. Khi sử dụng với một véc-tơ kiểu chuỗi ký tự sẽ trả lại giá trị là một véc-tơ kiểu số mà mỗi phần tử là số ký tự của phần tử tương ứng trong véc-tơ kiểu chuỗi ký tự
## [1] 1 2 1 1 1 2 2 4
Bằng cách kết hợp các hàm số trên véc-tơ và tương tác giữa véc-tơ với một biến, bạn đọc có thể tự tạo ra các hàm số, các phương pháp của riêng mình để giải quyết các vấn đề phức tạp hơn. Chẳng hạn như chúng ta muốn biết có bao nhiêu phần tử trong véc-tơ thỏa mãn một điều kiện nào đó, chúng ta có thể kết hợp hàm sum() với một biểu thức so sánh giữa véc-tơ với một số
x<-c(1,1,2,3,5,8,13,21) # véc-tơ x kiểu số
sum(x>10) # cho biết có bao nhiêu phần tử trong x lớn hơn 10## [1] 2
Khi thực hiện phép so sánh \(x > 10\), do \(x\) là một véc-tơ kiểu số nên phép so sánh sẽ trả lại giá trị là \(TRUE\) tại các vị trí mà kết quả so sánh là đúng và \(FALSE\) tại các vị trí còn lại. Khi kết hợp với hàm sum(), các giá trị \(TRUE\) sẽ được đổi thành số 1 và \(FALSE\) được đổi thành 0. Kết quả thu được sẽ là số lượng các giá trị \(TRUE\) trong phép so sánh, hay nói một cách khác, là số các phần tử trong \(x\) thỏa mãn điều kiện lớn hơn \(10\). Tất nhiên với véc-tơ \(x\) có độ dài 10 như ở trên, bạn đọc có thể nhìn được một cách trực quan mà không cần hỗ trợ của R. Nhưng thực tế thì các véc-tơ mà chúng ta cần thực hiện tính toán sẽ có độ dài lớn hơn rất nhiều và bạn đọc không thể không dùng phần mềm hỗ trợ. Chẳng hạn như bạn đọc muốn biết có bao nhiêu câu tweets của cựu tổng thống Donald Trump có nhiều hơn 10.000 lượt yêu thích, bạn có thể kết hợp sum() với biểu thức so sánh. Véc-tơ chứa số lượt yêu thích với mỗi câu tweet là cột \(favorite\_count\) trong dữ liệu \(trump\_tweets\)
x<-trump_tweets$favorite_count # véc-tơ kiểu số cho biết mỗi câu được like bao nhiêu lần
sum(x>10^4) # cho biết có bao nhiêu phần tử trong x lớn hơn 10^4## [1] 4958
Để biết tỷ lệ số câu tweet có số lượt yêu thích nhiều hơn 10.000, bạn đọc có thể kết hợp thêm với hàm length()
## [1] 0.2388132
Có rất nhiều cách kết hợp các hàm số lại để đạt được kết quả mong muốn. Một kết quả phân tích có thể đạt được bằng các cách kết hợp khác nhau. Để sử dụng thành thạo chỉ có một cách duy nhất là bạn đọc hãy thực hành nhiều trên R và tự đúc kết kinh nghiệm của mình
1.3.3 Lấy véc-tơ con từ một véc-tơ
Khi làm việc với véc-tơ, chúng ta thường phải lấy các phần tử của véc-tơ ra theo một thứ tự hoặc lấy các phần tử con thỏa mãn các điều kiện nào đó và lưu kết quả vào một véc-tơ mới. Kỹ thuật này sẽ được thảo luận dưới đây.
1.3.3.1 Hai cách lấy véc-tơ con từ một véc-tơ
Để lấy một phần tử con của một véc-tơ \(x\) chúng ta sử dụng dấu ngoặc vuông \([]\). Chẳng hạn như để lấy ra phần tử thứ \(1\), chúng ta sử dụng \(x[1]\). Số 1 trong trường hợp này được gọi là chỉ số. Nhắc lại với bạn đọc rằng chỉ số của các phần tử trong véc-tơ của R là bắt đầu từ \(1\) và phần tử cuối cùng trong véc-tơ có chỉ số bằng với độ dài của véc-tơ đó. Nếu chúng ta sử dụng chỉ số lớn hơn độ dài của véc-tơ, R sẽ trả lại giá trị là \(NA\).
x<-c(1,1,2,3,5,8,13,21) # véc-tơ x kiểu số
x[1] # lấy ra phần tử thứ nhất trong x## [1] 1
x[11] # độ dài của x là 10 nên giá trị trả lại sẽ là NA## [1] NA
Bạn đọc có thể đặt câu hỏi là điều gì xảy ra nều sử dụng chỉ số \(0\) hoặc chỉ số là số âm. Hãy nói về chỉ số \(0\) trước. Khi gọi phần tử ở vị trí thứ 0 trong một véc-tơ bạn đọc sẽ nhận được một phần tử rỗng. Khái niệm rỗng có thể hiểu giống như khái niệm rỗng khi nói về một tập hợp không có phần tử. Tùy theo kiểu giá trị của véc-tơ ta sẽ có một phần tử rỗng với kiểu giá trị tương ứng
| Kiểu véc-tơ | Giá trị tại chỉ số 0 |
|---|---|
| Kiểu số nguyên | integer(0) |
| Kiểu số thực | numeric(0) |
| Kiểu logical | logical(0) |
| Kiểu chuỗi ký tự | character(0) |
| Kiểu ngày tháng | Date of length 0 |
| Kiểu thời gian chính xác | POSIXct of length 0 |
Khi sử dụng chỉ số âm đối với véc-tơ, R hiểu rằng chúng ta đang loại đi các phần tử. Thật vậy, \(x[-1]\) sẽ trả lại kết quả là một véc-tơ giống với véc-tơ \(x\) sau khi loại đi phần tử thứ nhất. Với số tự nhiên \(k, (k \in \mathbb{N}),\) \(x[-k]\) sẽ trả lại kết quả là véc-tơ \(x\) sau khi loại đi phần tử thứ \(k\). Nếu \(k\) lớn hơn độ dài của véc-tơ \(x\), véc-tơ nhận được sẽ đúng bằng \(x\). Sử dụng chỉ số âm cũng có thể hiểu là một cách để lấy một véc-tơ con từ một véc-tơ ban đầu. Đây là cách lấy véc-tơ con bằng cách sử dụng véc-tơ chỉ số kiểu số nguyên.
Có hai cách để lấy véc-tơ con từ một véc-tơ ban đầu, đó là
- Sử dụng một véc-tơ chỉ số kiểu số nguyên; và
- Sử dụng một véc-tơ chỉ số kiểu logic.
Từ véc-tơ \(x\) ban đầu, để lấy ra một véc-tơ con, trong trường hợp chúng ta đã biết chính xác các vị trí và thứ tự của các phần tử con mà chúng ta muốn lấy ra, chúng ta có thể lưu vị trí của các phần tử con này vào một véc-tơ khác tạm gọi là véc-tơ \(y\). Véc-tơ \(y\) còn được gọi là véc-tơ chỉ số. Sau đó, chúng ta chỉ cần sử dụng câu lệnh \(x[y]\) để lấy ra các phần tử của \(x\) tại các vị trí được lưu ở véc-tơ \(y\). Thật vậy, hãy thử quan sát ví dụ sau
x<-c("cam","táo","kiwi","chuối","nho") # véc-tơ x kiểu chuỗi ký tự
y<-c(3,5,2,3,1) # lấy ra véc-tơ con tại chỉ số y
x[y] # thứ thự trong véc-tơ con là x[3] -> x[5] -> x[2] -> x[3] -> x[1]## [1] "kiwi" "nho" "táo" "kiwi" "cam"
Nếu trong véc-tơ chỉ số có giá trị lớn hơn độ dài của véc-tơ ban đầu, R sẽ trả lại giá trị là \(NA\) tại vị trí đó
x<-c("cam","táo","kiwi","chuối","nho") # véc-tơ x kiểu chuỗi ký tự
y<-c(3,5,2,10,3,1) # chỉ số 10 lớn hơn độ dài véc-tơ (5)
x[y] # vị trí thứ tư trong véc-tơ con sẽ là NA## [1] "kiwi" "nho" "táo" NA "kiwi" "cam"
Nếu chúng ta sử dụng véc-tơ chỉ số là số âm, R sẽ hiểu rằng chúng ta đang muốn loại đi một hay một số phần tử nào đó.
x<-c("cam","táo","kiwi","chuối","nho") # véc-tơ x kiểu chuỗi ký tự
y<-c(-3,-5,-2,-3) # véc-tơ chỉ số toàn số âm
x[y] # nhận được véc-tơ con sau khi loại đi các số thứ 2,3,5 trong y (chỉ còn x[1] rồi x[4])## [1] "cam" "chuối"
R sẽ báo lỗi nếu véc-tơ chỉ số \(y\) chứa cả số âm và số dương. Bạn đọc cần lưu ý vấn đề này. Trong thực tế, ít khi chúng ta biết chính xác vị trí mà chúng ta muốn lấy ra, hay nói cách khác chúng ta không thể trực tiếp khai báo giá trị vào véc-tơ chỉ số \(y\). Thông thường \(y\) sẽ là kết quả của các hàm số tạo chỉ số. Các hàm which() và hàm match() được thảo luận ở phần tiếp theo của cuốn sách là các phương pháp tuyệt vời để tạo ra các véc-tơ chỉ số kiểu số.
Phương pháp thứ hai để lấy một véc-tơ con từ véc-tơ \(x\) đó là sử dụng véc-tơ chỉ số kiểu logic. Cách lấy này sẽ rất thuận tiện khi bạn đọc muốn lấy ra một véc-tơ con của \(x\) bao gồm các phần tử thỏa mãn một điều kiện nào đó. Véc-tơ chỉ số, tạm gọi là véc-tơ \(y\), được tạo ra từ một phép so sánh, sau đó câu lệnh \(x[y]\) sẽ trả lại giá trị là một véc-tơ con của \(x\) bao gồm các phần tử mà vị trí tương ứng của nó trong véc-tơ \(y\) là \(TRUE\). Lấy véc-tơ con bằng cách này, bạn đọc hãy luôn để độ dài của véc-tơ \(y\) bằng độ dài của véc-tơ \(x\). Khi độ dài của \(y\) không bằng độ dài của \(x\), câu lệnh \(x[y]\) vẫn trả lại kết quả, tuy nhiên hiểu được kết quả là khá phức tạp. Do đó chúng tôi khuyên bạn đọc hãy luôn đảm bảo rằng véc-tơ chỉ số kiểu logic và véc-tơ ban đầu luôn có cùng độ dài.
Giả sử với véc-tơ \(x\) chứa tên các loại quả, chúng ta muốn lấy ra tên các loại quả có tên dài hơn 3 ký tự. Chúng ta không biết chính xác các quả này nằm ở vị trí nào trong \(x\) nên không thể tạo véc-tơ chỉ số kiểu số. Trong trường hợp này, chúng ta sẽ tạo một véc-tơ chỉ số \(y\) kiểu logic như sau
x<-c("cam","táo","kiwi","chuối","nho") # véc-tơ x kiểu chuỗi ký tự
y<-(nchar(x)>3) # y có độ dài bằng x, giá trị TRUE tại vị trí có độ dài > 3
y # hiển thị giá trị của y## [1] FALSE FALSE TRUE TRUE FALSE
x[y] # trả lại giá trị trong x mà vị trí tương ứng trong y là TRUE## [1] "kiwi" "chuối"
Đây là cách lấy ra các véc-tơ con rất hiệu quả khi làm việc với dữ liệu. Các cột dữ liệu là các véc-tơ có cùng độ dài, do đó chỉ số \(y\) có thể được tạo thành từ phép so sánh một cột dữ liệu và véc-tơ \(x\) lại là một cột dữ liệu khác. Chẳng hạn như chúng ta muốn lấy ra các câu tweet của cựu tổng thống Donald Trump được like nhiều hơn 10.000 lần và lưu vào một véc-tơ, chúng ta chỉ cần thực hiện như sau:
x<-trump_tweets$text # véc-tơ x chứa tất cả các câu tweet
y<-trump_tweets$favorite_count > 10^4 # y là chỉ số, nhận giá trị TRUE tại các câu nhiều hơn 10.000 like
z<-x[y] # z chỉ chứa các câu tweet nhiều hơn 10.000 likeĐiều gì xảy ra nếu độ dài của \(y\) không giống như độ dài của \(x\). Trong trường hợp \(y\) có độ dài nhỏ hơn độ dài của \(x\), R sẽ tạo ra một véc-tơ \(y1\) có độ dài bằng với độ dài của \(y\) bằng cách lặp lại giá trị của \(y\) cho đến khi véc-tơ thu được có độ dài bằng \(x\). Hãy quan sát ví dụ sau
x<-c("cam","táo","kiwi","chuối","nho") # véc-tơ x kiểu chuỗi ký tự độ dài 5
y<-c(TRUE,FALSE) # y có độ dài là 2, nhỏ hơn 5
x[y] # là véc-tơ có độ dài 5## [1] "cam" "kiwi" "nho"
Kết quả thu được tương tự như khi chúng ta thực hiện phép lấy véc-tơ con thông qua một véc-tơ chỉ số \(y1\) có độ dài bằng 5 như sau
y1<-rep(y,3) # lặp lại y cho đến khi có độ dài lớn hơn x (độ dài của y1 là 6 > 5)
y1<-y1[1:length(x)] # chỉ số y1 là chỉ lấy đến đúng độ dài của x
x[y1] # cho kết quả giống như khi viết x[y]## [1] "cam" "kiwi" "nho"
Nếu độ dài của véc-tơ chỉ số \(y\) lớn hơn độ dài của \(x\), tại các vị trí của \(y\) mà chỉ số vẫn nhỏ hơn hoặc bằng chiều dài của \(x\), việc lấy ra phần tử con vẫn theo quy tắc thông thường, nghĩa là lấy ra các phần tử tương ứng với giá trị \(TRUE\) và bỏ qua các phần tử tương ứng với giá trị \(FALSE\). Tại các vị trí của \(y\) mà chỉ số lớn hơn chiều dài của \(x\), R sẽ bỏ qua các phần tử có giá trị là \(FALSE\) và sẽ trả lại giá trị là \(NA\) mỗi khi gặp giá trị \(TRUE\). Bạn đọc có thể quan sát ví dụ sau
x<-c("cam","táo","kiwi","chuối","nho") # véc-tơ x kiểu chuỗi ký tự độ dài 5
y<-c(nchar(x)>3,FALSE,TRUE) # y có độ dài là 7, vị trí thứ 6 là FALSE, thứ 7 là TRUE
x[y] # x sẽ là các loại quả có tên dài hơn 3 ký tự, theo sau là NA do y[7] là TRUE## [1] "kiwi" "chuối" NA
Do sự phức tạp khi tương tác giữa các véc-tơ có không cùng độ dài nên chúng tôi khuyên bạn đọc hãy luôn luôn thực hiện các phép tính toán với các véc-tơ có cùng độ dài để kiểm soát được kết quả khi làm việc với R. Trong phần tiếp theo chúng ta sẽ thảo luận về các hàm số để tạo ra véc-tơ chỉ số.
1.3.4 Các hàm tạo chỉ số trong véc-tơ
Có một nhóm các hàm số thường được sử dụng khi làm việc với chỉ số của các phần tử trong véc-tơ. Các hàm số này có thể được phỏng theo bằng cách kết hợp một vài kỹ thuật chỉ số đã đề cập đến ở chương trước. Tuy nhiên chúng tôi khuyên bạn đọc nên sử dụng các hàm có sẵn được trình bày trong phần này bởi sự tiện lợi và sự dễ hiểu của các dòng lệnh. Các hàm số liên quan đến chỉ số của véc-tơ được liệt kê trong bảng sau
| Hàm số | Ý nghĩa |
|---|---|
| which() | Chỉ số của các phần tử nhận giá trị là TRUE của một véc-tơ kiểu logical |
| match() | Cho biết chỉ số của một phần tử nằm trong một véc-tơ khác |
| %in% | Trả lại giá trị là TRUE nếu một phần tử của một véc-tơ có nằm trong một véc-tơ khác |
| rank | Trả lại giá trị là thứ tự của phần tử khi xếp véc-tơ theo thứ tự TĂNG dần |
| order() | Trả lại giá trị là chỉ số của các phần tử sau khi xếp theo thứ tự TĂNG dần |
1.3.4.1 Hàm which()
Hàm which() áp dụng trên một véc-tơ kiểu logic và cho biết các vị trí nào trong véc-tơ logic có giá trị là \(TRUE\). Có hai biến thể của hàm which() thường được sử dụng là which.min() và which.max() cho biết chỉ số (vị trí) của giá trị lớn nhất và chỉ số của giá trị nhỏ nhất.
x<-c(20,40,60,50,30,10) # Véc-tơ kiểu số
which(x>40) # Các chỉ số (vị trí) trong véc-tơ x có giá trị > 40## [1] 3 4
which.min(x) # Số nhỏ nhất trong x (số 10) nằm ở vị trí nào## [1] 6
which.max(x) # Số lớn nhất trong x (số 60) nằm ở vị trí nào ## [1] 3
Trong trường hợp \(x\) có nhiều giá trị bằng với giá trị lớn nhất hoặc nhiều giá trị bằng với giá trị nhỏ nhất, các hàm which.min() và which.max() luôn luôn trả lại giá trị là chỉ số nhỏ hơn.
x<-c(20,40,60,50,30,10,60,10) # Véc-tơ kiểu số
which.min(x) # Số nhỏ nhất trong x (số 10) nằm ở vị trí nào## [1] 6
which.max(x) # Số lớn nhất trong x (số 60) nằm ở vị trí nào ## [1] 3
Bạn đọc sử dụng hàm which() để tạo ra véc-tơ chỉ số khi muốn lấy ra các phần tử của một véc-tơ thỏa mãn một điều kiện nào đó. Ví dụ như chúng ta muốn lấy ra các các câu tweet của Donald Trum có nhiểu hơn 10.000 lượt yêu thích bằng một véc-tơ chỉ số:
x<-trump_tweets$text # Véc-tơ chứa tất cả các câu tweet
y<-which(trump_tweets$favorite_count>10^4) # Véc-tơ kiểu số cho biết các chỉ số (vị trí) nào có nhiều hơn 10.000 lượt thích
z<-x[y] # z chứa tất cả các câu tweet có nhiều hơn 10.000 like
1.3.4.2 Hàm match() và toán tử %in\%
Hàm \(match()\) là hàm số cho phép tương tác giữa hai véc-tơ có độ dài khác nhau. Cho \(x\) và \(y\) là hai véc-tơ có cùng kiểu, câu lệnh match(y,x) sẽ trả lại giá trị là một véc-tơ, tạm gọi là \(z\), có độ dài bằng với độ dài của véc-tơ \(y\), đồng thời \(z[1]\) cho biết \(y[1]\) có chỉ số (nằm ở vị trí) nào trong véc-tơ \(x\); \(z[2]\) cho biết \(y[2]\) có chỉ số (nằm ở vị trí) nào trong véc-tơ \(x\),… Các phần tử của \(y\) không xuất hiện trong \(x\) sẽ cho giá trị tương ứng trong \(z\) là \(NA\).
x<-c(20,40,60,50,30,10) # Véc-tơ x kiểu số
y<-c(60,10,70) # véc-tơ y kiểu số
match(y,x) # cho biết từng phần tử của y nằm ở vị trí thứ bao nhiêu trong x## [1] 3 6 NA
Chúng ta có thể thấy rằng giá trị 70 không xuất hiện trong \(x\) nên giá trị thứ 3 trong véc-tơ kết quả là \(NA\). Lưu ý rằng hàm match() luôn luôn tìm đến chỉ số đầu tiên trong véc-tơ \(x\) có giá trị khớp với giá trị của véc-tơ \(y\), nghĩa là trong \(x\) có nhiều hơn một giá trị khớp với giá trị của \(y\), hàm match() cho kết quả là chỉ số nhỏ hơn. Bạn đọc quán sát ví dụ dưới dây khi véc-tơ \(x\) có nhiều giá trị khớp với giá trị của \(y\):
x<-c(20,40,60,50,30,10,20,10) # véc-tơ x kiểu số, giá trị 10 và 20 xuất hiện nhiều lần
y<-c(10,20) # véc-tơ y kiểu số
match(y,x)## [1] 6 1
Các giá trị 10 và 20 của \(y\) xuất hiện hai lần trong \(x\), tuy nhiên hàm match() sẽ trả lại giá trị là 6 và 1 bởi vì số 10 xuất hiện lần đầu tiên ở vị trí thứ 6 trong \(x\) và số 20 xuất hiện lần đầu tiên ở vị trí thứ 1 trong \(x\).
Hàm match() trả lại kết quả là véc-tơ chỉ số nên sẽ phù hợp với việc lấy véc-tơ con theo chỉ số kiểu số. Một phương pháp khác để làm việc với chỉ số của véc-tơ là toán tử %in%. Toán tử %in% được sử dụng để cho biết mỗi phần tử của một véc-tơ có nằm trong một véc-tơ khác hay không. Câu lệnh y %in% x sẽ trả lại giá trị là một véc-tơ kiểu logic \(z\) có độ dài bằng với độ dài của \(y\), \(z[i]\) nhận giá trị là \(TRUE\) nếu \(y[i]\) có xuất hiện trong \(x\) và nhận giá trị là \(FALSE\) nếu \(y[i]\) không xuất hiện trong \(x\).
x<-c(20,40,60,50,30,10) # Véc-tơ x kiểu số
y<-c(60,10,70) # véc-tơ y kiểu số
y %in% x # cho biết từng phần tử của y có nằm trong x hay không## [1] TRUE TRUE FALSE
Hình vẽ dưới đây minh họa kết quả được trả ra của hàm match() và toán tử \%in\%

Hàm match() và toán tử \%in\% cho phép tương tác giữa các véc-tơ có độ dài khác nhau nên rất hiệu quả khi bạn đọc muốn kết nối nhiều dữ liệu khác nhau. Bạn đọc hãy đọc ví dụ dưới đây để hình dung cách sử dụng hàm match() khi kết nối hai dữ liệu.
Giả sử chúng ta có danh sách điểm học tại trường đại học của ba sinh viên ngành actuary có mã sinh viên lần lượt là “MSV001”, “MSV002”, “MSV003” khi học các môn học “Xác suất”, “Toán tài chính”, và “Đầu tư và thị trường tài chính”. Thông tin được lưu trong một dữ liệu tên là “diem_hoc_DH”. Sinh viên ngành actuary ngoài các môn học ở trường đại học có thể thi các môn học tại các hiệp hội nghề nghiệp actuary để lấy chứng chỉ hành nghề. Thông tin về điểm thi chứng chỉ được lưu trong dữ liệu có tên là “diem_chung_chi_Actuary”. Khi xét tốt nghiệp, sinh viên có quyền lấy điểm thi chứng chỉ tại các hiệp hội để thay thế cho điểm học tại trường đại học của môn học tương ứng nếu điểm thi chứng chỉ cao hơn. Dữ liệu về điểm thi tại trường đại học và thi chứng chỉ hành nghề như sau:
| Mã sinh viên | Môn học | Điểm thi |
|---|---|---|
| MSV001 | Xác suất | 5 |
| MSV002 | Xác suất | 7 |
| MSV003 | Xác suất | 9 |
| MSV001 | Toán tài chính | 10 |
| MSV002 | Toán tài chính | 6 |
| MSV003 | Toán tài chính | 8 |
| MSV001 | Đầu tư và thị trường tài chính | 9 |
| MSV002 | Đầu tư và thị trường tài chính | 5 |
| MSV003 | Đầu tư và thị trường tài chính | 10 |
| Mã sinh viên | Môn học | Điểm thi |
|---|---|---|
| MSV005 | Xác suất | 8 |
| MSV002 | Xác suất | 9 |
| MSV004 | Xác suất | 10 |
| MSV003 | Toán tài chính | 10 |
| MSV002 | Toán tài chính | 9 |
| MSV001 | Đầu tư và thị trường tài chính | 8 |
Để tìm được điểm thi chứng chỉ của viên trong bảng “diem_hoc_DH” chúng ta phải kết nối (sử dụng hàm match()) bảng này với bảng “diem_chung_chi_Actuary” thông qua mã sinh viên và tên môn học. Việc kết nối sẽ được thực hiện bằng cách tạo ra trên mỗi bảng một véc-tơ có gọi tên là \(key\) là tổ hợp của mã sinh viên và tên môn học.
Trước hết bạn đọc có thể tạo hai dữ liệu trên như sau:
# du lieu diem_hoc_DH
MSV <- rep(c( "MSV001", "MSV002", "MSV003"),3)
Mon_hoc <- c(rep("Xác suất",3),rep("Toán tài chính",3),rep("Đầu tư và thị trường tài chính",3))
Diem <- c(5,7,9,10,6,8,9,5,10)
diem_hoc_DH <- data.frame(MSV, Mon_hoc, Diem)
# du lieu diem_chung_chi_Actuary
MSV <- c("MSV005", "MSV002", "MSV004", "MSV003", "MSV002", "MSV001")
Mon_hoc <- c("Xác suất", "Xác suất", "Xác suất", "Toán tài chính", "Toán tài chính", "Đầu tư và thị trường tài chính")
Diem <- c(8,9,10,10,9,8)
diem_chung_chi_Actuary <- data.frame(MSV, Mon_hoc, Diem)Chúng ta tạo ra hai véc-tơ để kết nối hai bảng, véc-tơ tạo ra bằng cách kết hợp từ véc-tơ chứa mã sinh viên và véc-tơ tên môn học
diem_hoc_DH_key<- paste(diem_hoc_DH$MSV, diem_hoc_DH$Mon_hoc)
diem_chung_chi_Actuary_key<-paste(diem_chung_chi_Actuary$MSV, diem_chung_chi_Actuary$Mon_hoc)Toán tử \%in\% sẽ cho chúng ta biết những phần tử nào trong \(diem\_hoc\_DH\_key\) nằm trong \(diem\_chung\_chi\_Actuary\_key\), hay nói một cách khác, sinh viên nào trong bảng “diem_hoc_DH” có thi chứng chỉ tương ứng với môn học ở trường đại học:
y<-diem_hoc_DH_key %in% diem_chung_chi_Actuary_keyChỉ số \(y\) là kết quả của toán tử \%in\% nên sẽ có dạng logical. \(y\) có độ dài là 9 bằng với số dòng của dữ liệu \(diem\_hoc\_DH\) và cho biết tương ứng mỗi sinh viên có thi chứng chỉ môn học tương ứng hay không. Chẳng hạn như muốn tạo ra danh sách thi chứng chỉ của sinh viên lớp Actuary 60:
data.frame(MSV = diem_hoc_DH$MSV[y], # Lọc véc-tơ cột MSV bằng véc-tơ kiểu logic y
Diem = diem_hoc_DH$Mon_hoc[y]) # Lọc véc-tơ cột tên môn học bằng véc-tơ kiểu logic y ## MSV Diem
## 1 MSV002 Xác suất
## 2 MSV002 Toán tài chính
## 3 MSV003 Toán tài chính
## 4 MSV001 Đầu tư và thị trường tài chính
Để tìm được điểm thi chứng chỉ của các sinh viên lớp Actuary 60 chúng ta cần biết kết nối mã sinh viên và môn học từ bảng \(diem\_hoc\_DH\) đến bảng \(diem\_chung\_chi\_Actuary\) bằng cách sử dụng hàm match()
y<-match(diem_hoc_DH_key,diem_chung_chi_Actuary_key)Véc-tơ \(y\) có độ dài bằng 9, cho biết mỗi dòng của dữ liệu \(diem\_hoc\_DH\) tương ứng với dòng thứ bao nhiêu (chỉ số) của dữ liệu \(diem\_chung\_chi\_Actuary\). Giá trị \(NA\) trong \(y\) có ý nghĩa là dòng tương ứng của dữ liệu \(diem\_hoc\_DH\) không xuất hiện trong \(diem\_chung\_chi\_Actuary\) (sinh viên không thi chứng chỉ môn học tương ứng). Chúng ta có thể thêm một cột (véc-tơ) gọi là \(diem\_CT\) cho bảng \(diem\_hoc\_DH\)
diem_hoc_DH$diem_CT<-diem_chung_chi_Actuary$Diem[y] # lấy véc-tơ con bẳng chỉ số kiểu sốNhư vậy chúng ta đã có một dữ liệu với điểm học trên lớp và điểm thi chứng chỉ của các sinh viên
| Mã sinh viên | Môn học | Điểm thi | Điểm chứng chỉ |
|---|---|---|---|
| MSV001 | Xác suất | 5 | NA |
| MSV002 | Xác suất | 7 | 9 |
| MSV003 | Xác suất | 9 | NA |
| MSV001 | Toán tài chính | 10 | NA |
| MSV002 | Toán tài chính | 6 | 9 |
| MSV003 | Toán tài chính | 8 | 10 |
| MSV001 | Đầu tư và thị trường tài chính | 9 | 8 |
| MSV002 | Đầu tư và thị trường tài chính | 5 | NA |
| MSV003 | Đầu tư và thị trường tài chính | 10 | NA |
1.3.4.3 Hàm rank() và hàm order().
Hàm rank(x) trả lại giá trị là thứ tự (rank) của một phần tử trong véc-tơ \(x\) khi sắp xếp \(x\) theo thứ tự tăng dần. Thứ tự tăng dần ở đây được sử dụng đối với các véc-tơ kiểu chuỗi ký tự.
x<-c(20,40,60,50,30,10) # Véc-tơ x kiểu số
rank(x) # tương ứng với số lớn nhất (60) là chỉ số 6, tương ứng với 10 là chỉ số 1## [1] 2 4 6 5 3 1
Lưu ý rằng hàm rank() có một tùy chọn quan trọng là \(ties.method\). Khi bạn đọc không sử dụng tùy chọn này, giá trị mặc định là \("average"\). Tùy chọn \(ties.method\) chỉ có ý nghĩa khi \(x\) có các giá trị giống nhau. Trong trường hợp tất cả các phần tử trong \(x\) là đôi một khác nhau, bất kỳ tùy chọn nào đối với \(ties.method\) cũng trả lại một kết quả duy nhất.
Khi \(x\) có giá trị bị lặp lại, bạn đọc hãy quan sát ví dụ sau để thấy sự khác biệt khi sử dụng tùy chọn \(ties.method\)
x<-c(10,10,10,20,20) # Véc-tơ x kiểu số
rank(x,ties.method = "first") # Trong các giá trị bằng nhau, giá trị xuất hiện TRƯỚC có rank nhỏ hơn## [1] 1 2 3 4 5
rank(x,ties.method = "last") # Trong các giá trị bằng nhau, giá trị xuất hiện SAU có rank nhỏ hơn## [1] 3 2 1 5 4
rank(x,ties.method = "min") # Các giá trị bằng nhau có rank giống nhau và bằng rank nhỏ nhất## [1] 1 1 1 4 4
rank(x,ties.method = "max") # Các giá trị bằng nhau có rank giống nhau và bằng rank lớn nhất## [1] 3 3 3 5 5
rank(x,ties.method = "average") # Các giá trị bằng nhau có rank bằng nhau và bằng rank trung bình## [1] 2.0 2.0 2.0 4.5 4.5
rank(x,ties.method = "random") # Các giá trị bằng nhau có rank bằng nhau và bằng rank trung bình## [1] 3 2 1 5 4
Khi \(ties.method\) nhận giá trị là \("first"\), giá trị trả lại là \(1, 2, 3, 4, 5\). Ba số 10 liền nhau ở phần đầu của véc-tơ \(x\) được xếp thứ tự theo nguyên tắc số nào xuất hiện trước là có thứ tự NHỎ hơn, do đó thứ tự của ba số 10 này trong véc-tơ \(x\) khi xếp \(x\) theo thứ tự tăng dần là \(1 \rightarrow 2 \rightarrow 3\). Tương tự với hai số 20 ở cuối vec-tớ \(x\), số 20 xuất hiện trước được hiểu là có thứ tự trước số 20 xuất hiện sau, do đó thứ tự của hai số 20 sẽ là \(4 \rightarrow 5\)
Khi \(ties.method\) nhận giá trị là \("last"\), giá trị trả lại là \(3, 2, 1, 5, 4\). Ba số 10 liền nhau ở phần đầu của véc-tơ \(x\) được xếp thứ tự theo nguyên tắc số nào xuất hiện trước là có thử tự LỚN hơn, do đó thứ tự của ba số 10 này trong véc-tơ \(x\) khi xếp \(x\) theo thứ tự tăng dần là \(3 \rightarrow 2 \rightarrow 1\). Tương tự với hai số 20 ở cuối vec-tớ \(x\), số 20 xuất hiện trước được hiểu là có thứ tự LỚN hơn số 20 xuất hiện sau, do đó thứ tự của hai số 20 sẽ là \(5 \rightarrow 4\)
Khi \(ties.method\) nhận giá trị là \("min"\), giá trị trả lại là \(1, 1, 1, 4, 4\). Ba số 10 liền nhau ở phần đầu của véc-tơ \(x\) có thứ tự bằng nhau là 1. Đây chính là thứ tự nhỏ nhất của ba số khi xếp các số này theo tùy chọn \(ties.method = "first"\) (thứ tự của 3 số khi \(ties.method = "first"\) là 1, 2, 3). Tương tự ta có thứ tự của hai số 20 tiếp theo bằng nhau và bằng 4 (là giá trị nhỏ nhất trong (4,5)).
Tùy chọn \("max"\) ngược lại với \("min"\). Thứ tự của ba số 10 đầu tiên trong \(x\) đều bằng 3 - là số lớn nhất trong (1, 2, 3) đồng thời thứ tự của hai số 20 tiếp theo đều là 5 - là số lớn nhất trong (4,5).
Khi \(ties.method\) nhận giá trị là \("average"\), cũng là giá trị mặc định khi sử dụng hàm \(rank()\), thứ tự của ba số 10 ở đầu véc-tơ \(x\) được tính là trung bình của thứ tự khi xếp theo tùy chọn \("first"\). Thật vậy, thứ tư của ba số khi \(ties.method\) nhận giá trị là \("first"\) là \(1 \rightarrow 2 \rightarrow 3\). Thứ tự khi \(ties.method\) nhận giá trị là \("average"\) là \[ \cfrac{1 + 2 + 3}{3} = 2 \] và thứ tự của hai số 20 ở cuối véc-tơ là \[ \cfrac{4 + 5}{2} = 4.5 \]
Cuối cùng, khi \(ties.method\) nhận giá trị là \("random"\), thứ tự của ba số 10 ở đầu véc-tơ \(x\) là một \(hoán\) \(vị\) \(ngẫu\) \(nhiên\) của (1,2,3) - thứ tự của ba số khi \(ties.method\) nhận giá trị là \("first"\). Bạn đọc có thể thấy rằng hai lần gọi hàm \(rank()\) với tùy chọn \(ties.method = "random"\) có thể cho kết quả là khác nhau.
Một hàm số khác trả lại giá trị là chỉ số của véc-tơ là hàm order(). Câu lệnh y<-order(x) trả lại giá trị cho véc-tơ \(y\) là các chỉ số của \(x\) sao cho:
\(y[1]\) là chỉ số của số nhỏ nhất trong véc-tơ \(x\);
\(y[2]\) là chỉ số của số nhỏ thứ hai trong véc-tơ \(x\); …
số cuối cùng trong véc-tơ \(y\) là chỉ số của số lớn nhất trong véc-tơ \(x\).
Khi muốn lấy chỉ số của véc-tơ \(x\) nhưng theo thứ tự giảm dần bạn đọc sử dụng tùy biến \(decreasing = TRUE\) trong hàm order(). Khái niệm tăng dần và giảm dần cũng có thể hiểu cho các véc-tơ kiểu thời gian, kiểu factor hay kiểu chuỗi ký tự.
## [1] 6 1 5 2 4 3
order(x, decreasing = TRUE) # chỉ số khi xếp x theo thứ tự GIẢM dần## [1] 3 4 2 5 1 6
Hàm order(x) cho kết quả là 6 tại vị trí thứ nhất có nghĩa là số nhỏ nhất trong \(x\) nằm ở vị trí thứ sáu trong véc-tơ này (số 10). Vị trí thứ hai trong order(x) nhận giá trị là 1 có nghĩa là số nhỏ thứ hai trong \(x\) nằm ở vị trí thứ nhất trong véc-tơ này, và cứ tiếp tục như thế. Vị trí cuối cùng trong order(x) có giá trị là 3 có nghĩa là số lớn nhất trong véc-tơ \(x\) nằm ở vị trí thứ 3 trong véc-tơ này.
Hàm order(x) có thể được phỏng theo được bằng cách khớp chỉ số của véc-tơ \(x\) với hàm rank(x, ties.method = "first"), thật vậy:
x<-c(20,20,10,10,10) # véc-tơ kiểu số có các giá trị giống nhau
chiso<-1:length(x) # chỉ số tăng dần từ 1 đến độ dài của x
match(chiso,rank(x, ties.method = "first")) # match chiso với rank## [1] 3 4 5 1 2
order(x) # cho kết quả giống như ở trên## [1] 3 4 5 1 2
Sử dụng hàm order() bạn đọc có thể dễ dàng lấy ra các giá trị nhỏ (hoặc lớn) thứ \(k\) trong một véc-tơ. Chẳng hạn như bạn đọc muốn lấy ra câu tweet có sốt lượt yêu thích nhiều thứ hai của cựu tổng thống Donald Trump từ dữ liệu \(trump\_tweet\), bạn có thể sử dụng hàm order() như sau
y<-order(trump_tweets$favorite_count, decreasing = T)[2] # vị trí của câu tweet được like nhiều thứ 2
trump_tweets$text[y] # lấy ra câu tweet được like nhiều thứ hai## [1] "Why would Kim Jong-un insult me by calling me \"old,\" when I would NEVER call him \"short and fat?\" Oh well, I try so hard to be his friend - and maybe someday that will happen!"
1.4 Lập trình R
Để viết các chương trình phức tạp hơn trong R, bạn đọc sẽ cần kiểm soát tốt trình tự mà các dòng lệnh của mình. Một cách cơ bản để làm được việc này là thực hiện một số câu lệnh nhất định phụ thuộc vào một hoặc một số điều kiện hay còn gọi là viết các câu lệnh rẽ nhánh. Một cách kiểm soát khác là sử dụng vòng lặp nhằm lặp lại một nhóm các câu lệnh một số lần nhất định. Trong phần này, chúng ta sẽ khám phá những kiến thức lập trình cơ bản này trong ngôn ngữ lập trình R. Các kiến thức về lập trình bao gồm có cách sử dụng câu lệnh rẽ nhánh (if-else), cách sử dụng vòng lặp (for, while, và repeat) và một vài cấu trúc khác giúp bạn đọc điều khiển được cách thực hiện các dòng lệnh của mình.
1.4.1 Câu lệnh điều kiện
1.4.1.1 Câu lệnh \(if\) và \(if-else\)
Bạn đọc sử dụng câu lệnh điều kiệu để thông báo cho R biết một câu lệnh, hay một nhóm câu lệnh chỉ thực hiện khi một điều kiện nào đó được thực thi. Dưới đây là cách viết của câu lệnh if trong ngôn ngữ R
if ("Biểu thức điều kiện"){
"Nhóm các câu lệnh thực hiện khi biểu thức điều kiện là ĐÚNG"
}Bạn đọc có thể thực hiện một đoạn lệnh có biểu thức điều kiện cụ thể như sau
x<-1; y<-2 # Dòng lệnh 1: tạo biến x có giá trị là 1 và biến y có giá trị là 2
if (x<10){ # Dòng lệnh 2: Nếu x nhỏ hơn 10 thì thực hiện các câu lệnh nằm trong {}
y<-4 # Dòng lệnh 3: Thay đổi, gán giá trị y bằng 4
} # Dòng lệnh 4: kết thúc câu lệnh ifKhi thực hiện nhóm các câu lệnh ở trên, dòng lệnh thứ 3 chỉ được thực hiện nếu biểu thức điều kiện được viết trong dấu ngoặc () ở dòng lệnh thứ 2 nhận giá trị là TRUE. Nếu biểu thức điều kiện đó nhận giá trị là FALSE, R sẽ không thực hiện các dòng lệnh số 3. Sau khi R thực thi các dòng lệnh 1, biến \(x\) nhận giá trị là 1 nên phép so sánh \(x<10\) sẽ cho kết quả là \(TRUE\). Do đó, dòng lệnh 3 gán giá trị mới bằng 4 cho biến \(y\) sẽ được thực hiện. Bạn đọc có thể kiểm tra được rằng sau khi thực hiện đoạn lệnh ở trên, giá trị của biến \(y\) sẽ bằng 4 chứ không phải là 2 như khởi tạo ở dòng lệnh số 1.
Khi sử dụng câu lệnh điều kiện if, sẽ không có câu lệnh nào được thực hiện trong trường hợp biểu thức điều kiện nhận giá trị là sai. Trong thực tế, đa phần các đoạn lệnh rẽ nhánh sẽ có các câu lệnh phải thực thi khi biểu thức điều kiện nhận giá trị là sai. Để thực hiện được việc này, bạn đọc sử dụng câu lệnh if kết hợp với else như sau
if ("Biểu thức điều kiện"){
"Nhóm các câu lệnh thực hiện khi biểu thức điều kiện là ĐÚNG"
} else {
"Nhóm các câu lệnh thực hiện khi biểu thức điều kiện là SAI"
}Bạn đọc có thể quan sát sự thay đổi giá trị của biến \(y\) sau khi thực hiện đoạn lệnh như sau
x<-1; y<-2 # Dòng lệnh 1: tạo biến x có giá trị là 1 và biến y có giá trị là 2
if (x==10){ # Dòng lệnh 2: Nếu x bằng 10 thì thực hiện các câu lệnh nằm trong {} của if
y<-4 # Dòng lệnh 3: Thay đổi, gán giá trị y bằng 4
} else { # Dòng lệnh 4: Nếu x KHÁC 10 thì thực hiện các câu lệnh nằm trong {} của else
y<-8 # Dòng lệnh 5: Thay đổi, gán giá trị y bằng 4
} # Dòng lệnh 6: kết thúc câu lệnh if-elseDo biểu thức điều kiện \(x==10\) nhận giá trị là \(FALSE\) nên R sẽ không thực hiện dòng lệnh số 3 mà chuyển qua thực hiện dòng lệnh số 5. Giá trị của \(y\) sau khi thực hiện đoạn lệnh ở trên sẽ là 8. Nếu trong dòng lệnh 1, bạn đọc sửa giá trị của \(x\) thành 10 thay vì 1, dòng lệnh 3 sẽ được thực hiện và dòng lệnh số 5 không được thực hiện do đó giá trị của \(y\) sau khi thực hiện đoạn lệnh lúc này sẽ là 4.
Biểu thức điều kiện trong câu lệnh if phải là một biến kiểu logic. Nếu do sơ ý, biểu thức điều kiện là một véc-tơ của các biến kiểu logic, câu lệnh if sẽ chỉ tính đến giá trị đầu tiên trong véc-tơ.
dieukien<-c(TRUE,FALSE,FALSE)
if (dieukien){ # dieukien là một véc-tơ kiểu logic
print("Xin chào") #R CÓ chạy dòng lệnh này
} # kết thúc câu lệnh ifBạn đọc có thể sẽ gặp câu lệnh ifelse() trong các đoạn câu lệnh của R. Tuy nhiên đây không phải là cách viết của câu lệnh rẽ nhánh. Hàm ifelse() được sử dụng khi muốn tạo ra một véc-tơ từ hai véc-tơ dựa trên giá trị của một véc-tơ kiểu logic. Cách sử dụng ifelse() được minh họa thông qua ví dụ dưới đây
## [1] "lẻ" "chẵn" "lẻ" "chẵn" "lẻ" "chẵn" "lẻ" "chẵn" "lẻ" "chẵn"
Hàm ifelse() ở trên sẽ tạo ra một véc-tơ có độ dài bằng với véc-tơ \(x\) và tương ứng với các vị trí cho kết quả là \(x\) chia hết cho 2 sẽ có giá trị là “chẵn” và tương ứng với các vị trí mà \(x\) không chia hết cho 2 sẽ có gía trị “lẻ”.
Khi sử dụng câu lệnh rẽ nhánh để thực hiện các yêu cầu phức tạp hơn, bạn đọc thường phải sử dụng các câu lệnh if và else lồng vào nhau để có được kết quả. Bạn đọc có thể quan sát ví dụ sau: để viết một đoạn câu lệnh để trả ra màn hình giá vé vào rạp chiếu phim của một khách hàng dựa trên độ tuổi và việc có thẻ thành viên hay không như bảng ở dưới đây, bạn đọc không thể chỉ dùng một câu lệnh điều kiện duy nhất.
| Độ tuổi | Có phải thành viên | Giá vé |
|---|---|---|
| Trẻ em (dưới 6 tuổi) | Thành viên | 70.000 đồng |
| Người lớn | Thành vien | 120.000 đồng |
| Trẻ em (dưới 6 tuổi) | Không phải thành viên | 100.000 đồng |
| Người lớn | Không phải thành viên | 150.000 đồng |
Giả sử biến \(Age\) là biến kiểu số cho biết độ tuổi của khách hàng và biến \(Member\) là biến kiểu logic nhận giá trị \(TRUE\) nếu khách hàng là thành viên và \(FALSE\) nếu khách hàng không phải là thành viên. Bạn đọc có thể sử dụng câu lệnh điều kiện để in ra màn hình giá vé của khách hàng đó bằng một trong hai cách như sau
# Cách thứ nhất: sử dụng bốn câu lệnh if
Age<-50; Member<-TRUE # tạo giá trị cho các biến Age, Member
if ((Age < 6) & Member){ # nếu khách hàng dưới 6 tuổi và là thành viên
print("70.000 đồng")
}
if ((Age < 6) & Member){ # nếu khách hàng trên 6 tuổi và là thành viên
print("100.000 đồng")
}
if ((Age < 6) & Member){ # nếu khách hàng dưới 6 tuổi và không phải thành viên
print("120.000 đồng")
}
if ((Age < 6) & Member){ # nếu khách hàng trên 6 tuổi và không phải thành viên
print("150.000 đồng")
}1.4.2 Vòng lặp
Vòng lặp là một cơ chế lập trình với mục đích để R lặp đi lặp lại việc chạy một dòng lệnh hay một đoạn lệnh cụ thể. Có hai kiểu viết lặp đó là vòng lặp for hoạt động theo cách cho một phần tử nhận lần lượt từng giá trị trong một véc-tơ và vòng lặp while hoạt động theo cách lặp lại một đoạn mã cho đến khi một điều kiện cụ thể nhận giá trị là \(FALSE\). Cách thức hoạt động kiểu vòng lặp cũng có thể được áp dụng khi sử dụng nhóm các hàm apply() trong R và sẽ được thảo luận ở một phần riêng của cuốn sách.
1.4.2.1 Vòng lặp for
Những câu lệnh sau dùng để in ra màn hình tất cả các giá trị nằm trong véc-tơ \(qua\) bằng cách sử dụng một vòng lặp for
qua = c("chuối", "táo", "cam", "chanh") # Vec-tơ chứa tên các loại quả
for (ten in qua){ # cho biến ten nhận lần lượt các giá trị trong vec-tơ qua
print(ten) # in ten ra màn hình
} # kết thúc vòng lặp for## [1] "chuối"
## [1] "táo"
## [1] "cam"
## [1] "chanh"
Các dòng lệnh bắt đầu từ for đến kết thúc dấu ngoặc \({}\) của vòng lặp có nghĩa là cho một biến \(ten\) nhận lần lượt các giá trị trong véc-tơ \(qua\) từ giá trị ở vị trí thứ nhất đến giá trị ở vị trí cuối cùng. Với mỗi giá trị mà biến \(ten\) nhận được, đoạn lệnh thực hiện nhóm các câu lệnh nằm trong dấu ngoặc \({}\) của vòng lặp for một lần. Trong đoạn lệnh ở trên các câu lệnh được lặp lại là câu lệnh \(print\) với tham số là biến \(ten\).
Bạn đọc hãy thử một ví dụ khó hơn một chút, chẳng hạn như bạn muốn tính tổng các số trong một véc-tơ \(x\) và không sử dụng hàm sum() có sẵn trong R. Bạn có thể thực hiện việc này bẳng một vòng lặp for như sau:
- Cho biến tên \(tong\) nhận giá trị bằng 0. \(tong\) sẽ là giá trị của tổng sau khi kết thúc vòng lặp
tong<-0- Cho một biến tên \(gia_tri\) nhận lần lượt các giá trị trong véc-tơ bắt đầu từ vị trí thứ nhất, tại mỗi lần lặp tăng giá trị biến \(tong\) lên đúng bằng giá trị của \(gia_tri\)
for (gia_tri in x){
tong<-tong + gia_tri
}- Sau khi vòng lặp \(for\) chạy qua tất cả các giá trị trong véc-tơ cần tính tổng, biến \(tong\) sẽ chứa giá trị của tổng các số trong véc-tơ.
print(tong)Giả sử \(x\) là véc-tơ \(Airpassengers\) - là một véc-tơ kiểu chuỗi thời gian có sẵn trong R chứa thông tin về số lượng khách hàng đi máy bay hàng tháng, đơn vị là nghìn người, tính từ tháng 1 năm 1949 đến tháng 12 năm 1960. Chúng ta sử dụng vòng lặp for để tính tổng các số trong véc-tơ sau đó so sánh kết quả với hàm sum() có sẵn.
tong<-0 # Tạo biến tên tong nhận giá trị 0
for (gia_tri in AirPassengers){ # cho biến gia_tri nhận lần lượt các giá trị trong Airpassengers
tong<-tong + gia_tri # tăng tong thêm giá trị bằng gia_tri
} # kết thúc vòng lặp
tong # in tong ra màn hình## [1] 40363
sum(AirPassengers) # hàm sum() có sẵn cũng cho kết quả tương tự## [1] 40363
Lời khuyên của chúng tôi là bạn đọc hãy luôn cố gắng viết câu lệnh trong R dưới dạng đối tượng vec-tơ nếu có Sử dụng véc-tơ trong R hiệu quả hơn nhiều cả về thời gian chạy lẫn sự đơn giản của các dòng lệnh. Thật vậy, bạn đọc có thể xem ví dụ dưới đây khi sử dụng vòng lặp for cho những véc-tơ có độ dài lớn và so sánh với tính toán theo vec-tơ. Véc-tơ được sử dụng để kiểm tra tính hiệu quả là véc-tơ có độ dài \(10^9\) (1 tỷ phần tử).
my_vector<-rep(1,10^9)
## Tính tổng véc-tơ có độ dài 10^9 bằng vòng lặp
start<-proc.time()
tong<-0
for (value in my_vector){
tong<-tong+value
}
proc.time()-start## user system elapsed
## 17.53 0.00 17.54
## Tính tổng véc-tơ có độ dài 10^9 bằng véc-tơ
start<-proc.time()
tong<-sum(my_vector)
proc.time()-start## user system elapsed
## 0.81 0.00 0.81
Bạn đọc có thể thấy rằng trên máy tính của chúng tôi, sử dụng vòng lặp for để tính tổng các số trong véc-tơ có độ dài \(10^9\) mất khoảng 25 giây trong khi dùng hàm sum() trực tiếp trên véc-tơ chỉ mất hơn 1 giây.
Trong các ví dụ ở trên, chúng tôi sử dụng trực tiếp giá trị trong véc-tơ để thực hiện vòng lặp. Bạn đọc cũng có thể sử dụng vòng lặp theo chỉ số của véc-tơ và cho kết quả tương tự. Chẳng hạn như đối với véc-tơ \(qua\), bạn đọc có thể cho một chỉ số nhận giá trị lần lượt từ 1 đến độ dài của véc-tơ \(qua\) để lấy từng phần tử của véc-tơ \(qua\):
for (i in 1:length(qua)){ # i sẽ nhận giá trị lần lượt 1,2,3,4
print(qua[i]) # in ra giá trị thứ i trong véc-tơ qua
} # kết thúc vòng lặp## [1] "chuối"
## [1] "táo"
## [1] "cam"
## [1] "chanh"
Trong nhiều trường hợp, bạn đọc cần phải sử dụng một vòng lặp \(for\) nằm trong một vòng lặp \(for\) khác để giải quyết được vấn đề của mình. Ví dụ như bạn cần in ra tất cả các cách kết hợp giữa hai cách pha chế là “Nước ép” và “Sinh tố” với bốn loại quả ở trên. Bạn đọc cần sử dụng 2 vòng lặp \(for\) lồng nhau để làm được việc này
pha_che<-c("Nước ép", "Sinh tố") # 2 cách pha chế
for (i in 1:length(pha_che)){ # i sẽ nhận giá trị lần lượt 1,2
for (j in 1:length(qua)){ # VỚI MỐI i, j sẽ nhận giá trị lần lượt 1,2,3,4
print(paste(pha_che[i],qua[j],sep=" ")) # in ra màn hình pha chế và quả
} # kết thúc vòng lặp của j với mỗi i
} # kết thúc vòng lặp của i## [1] "Nước ép chuối"
## [1] "Nước ép táo"
## [1] "Nước ép cam"
## [1] "Nước ép chanh"
## [1] "Sinh tố chuối"
## [1] "Sinh tố táo"
## [1] "Sinh tố cam"
## [1] "Sinh tố chanh"
Trong ví dụ ở trên, tổng số lần câu lệnh print() được lặp là \(4 \times 2 = 8 (\text{lần})\). Mỗi khi viết vòng lặp for, đặc biệt là khi viết các vòng lặp lồng vào nhau, bạn đọc hãy luôn cân nhắc thời gian R chạy vòng lặp. Một cách để kiểm tra thời gian vòng lặp chạy là thay vì cho chỉ số chạy qua độ dài của cả véc-tơ thì hãy cho vòng lặp thực hiện với một số lượng nhỏ chỉ số ban đầu để ước tính ra tổng thời gian. Nói một cách đơn giản, vòng lặp \(for\) chạy qua 100 giá trị ban đầu của véc-tơ sẽ mất thời gian bằng khoản \(\cfrac{1}{100}\) thời gian để chạy vòng lặp qua 10.000 giá trị của toàn bộ véc-tơ. Thời gian để thực hiện các vòng lặp for lồng nhau sẽ tăng lên theo cấp số nhân.
1.4.2.2 Vòng lặp while
Vòng lặp for được gọi là vòng lặp xác định vì nếu không có thêm các câu lệnh đặc biệt, người viết câu lệnh sẽ biết trước được số lần vòng lặp thực hiện. Một cách khác để thực hiện vòng lặp là sử dụng vòng lặp while. Đây là kiểu vòng lặp không xác định, nghĩa là trong nhiều trường hợp người viết câu lệnh sẽ không biết trước được sẽ vòng lặp sẽ được thực hiện bao nhiêu lần. Trước khi nói kỹ hơn về khái niệm không xác định, bạn đọc hãy làm quen với cấu trúc của vòng lặp while trước. Cách viết một vòng lặp while như sau
while (y){ # y là một biến kiểu logic
"Đoạn câu lệnh"
}Nguyên tắc hoạt động của vòng lặp while là thực hiện “Đoạn câu lệnh” nằm giữa dấu \({}\) nếu giá trị của \(y\) là \(TRUE\) và bỏ qua vòng lặp nếu giá trị của \(y\) là \(FALSE\). Nếu \(y\) nhận giá trị là \(TRUE\) và trong “Đoạn câu lệnh” không có các dòng lệnh tác động làm thay đổi giá trị của \(y\) thì \(y\) sẽ luôn luôn nhận giá trị là \(TRUE\) và khi đó vòng lặp sẽ lặp vô hạn.
Vòng lặp while dưới đây sẽ in ra tên các phần tử của véc-tơ \(qua\) bằng cách sử dụng một chỉ số tăng dần và chỉ thoát ra khỏi vòng lặp nếu chỉ số đó vượt qua độ dài của véc-tơ:
qua = c("chuối", "táo", "cam", "chanh") # Vec-tơ chứa tên các loại quả
i<-1
while (i <= length(qua)){ # TRUE cho đến khi i = 5
print(qua[i]) # in ra màn hình phần tử thứ i
i<-i+1 # tăng i lên dần để thoát ra khỏi vòng lặp
} # kết thúc vòng lặp while## [1] "chuối"
## [1] "táo"
## [1] "cam"
## [1] "chanh"
print(i) # kiểm tra giá trị của i khi thoát ra khỏi vòng lặp## [1] 5
Trong ví dụ ở trên chúng ta đã biết chính xác khi nào chúng ta sẽ dừng lại vòng lặp nên việc sử dụng vòng lặp while sẽ phức tạp hơn vòng lặp for. Vòng lặp while sẽ phát huy hiệu quả khi bạn đọc không biết chính xác khi nào chúng ta nên dừng việc thực hiện lặp các câu lệnh.
Hãy lấy ví dụ khi bạn đọc muốn kiểm tra xem một số tự nhiên \(n\) bất kỳ có phải là số nguyên tố hay không. Xin được nhắc lại rằng số nguyên tố là các số tự nhiên chỉ có hai ước số là số 1 và chính nó. Để kiểm tra xem số \(n\) có phải là số nguyên tố hay không, bạn đọc cần kiểm tra xem \(n\) có chia hết cho số nguyên dương nào từ 2 đến số tự nhiên là phần nguyên của \(\sqrt{n}\) hay không. Số phần nguyên của \(\sqrt{n}\) ký hiệu là \([\sqrt{n}]\). Nếu \(n\) chia hết cho một số bất kỳ từ 2 đến \([\sqrt{n}]\), \(n\) không phải là số nguyên tố. Theo nguyên tắc này bạn đọc có thể viết một vòng lặp for chạy từ \(2\) đến \([\sqrt{n}]\) và kiểm tra xem \(n\) có chia hết cho số nào trong dãy này không. Tuy nhiên vòng lặp for như vậy sẽ luôn luôn phải lặp lại \([\sqrt{n}] - 1\) lần. Viết vòng lặp while trong trường hợp này sẽ hiệu quả hơn rất nhiều bởi chỉ cần \(n\) chia hết cho 1 số nào đó chúng ta có thể kết thúc ngay vòng lặp và kết luận \(n\) không phải là số nguyên tố.
n<-123454321 # số nguyên dương bất kỳ
ket_qua<-TRUE # kết quả sẽ thay đổi nếu n chia hết cho 1 số nào đó
uoc_so<-2
while( ket_qua & (uoc_so < n^0.5) ){ # tiếp tục lặp nếu ket_qua = TRUE VÀ ước số < n^0.5
if(n %% uoc_so == 0){
ket_qua<-FALSE # thay đổi giá trị của ket_qua nếu n chia hết cho uoc_so
}
uoc_so<-uoc_so + 1 # tăng ước số thêm 1
}
ket_qua # TRUE nến n nguyên tố## [1] FALSE
Hãy thử áp dụng vòng lặp while trên một ví dụ khác liên quan đến dữ liệu \(trump\_tweet\). Chẳng hạn như bạn đọc muốn tìm ra thời điểm đầu tiên mà một câu tweet được like nhiều hơn 10.000 lần. Câu hỏi này khá dễ nếu chúng ta tư duy theo tương tác véc-tơ. Tuy nhiên chúng tôi muốn bạn đọc suy nghĩ theo hướng sử dụng vòng lặp. Chúng ta sẽ sử dụng một chỉ số tăng dần từ 1 và kiểm tra xem câu tweet đó có nhiều hơn 10.000 like hay không và chỉ dừng lại việc kiểm tra nếu gặp câu tweet nhiều hơn 10.000 like. Chúng ta không biết chính xác khi nào sẽ dừng lại, do đó sử dụng vòng lặp while sẽ hợp lý trong trường hợp này
kiem_tra<-TRUE
i<-0
while(kiem_tra){ # chắc chắn có câu nhiều hơn 10.000 like nên không cần hạn chế i
i<-i+1 # tăng chỉ số i
kiem_tra<-trump_tweets$favorite_count[i] <= 10^4 # tiếp tục lặp nếu số like <=10^4
}
trump_tweets$favorite_count[i] # chỉ số i là chỉ số nhỏ nhất mà số like nhiều hơn 10.000## [1] 15457
trump_tweets$created_at[i] # thời điểm viết câu tweet đó## [1] "2011-12-21 15:36:36 EST"
Mặc dù phần này của cuốn sách đang viết về vòng lặp nhưng chúng tôi muốn nhắc lại rằng bạn đọc hãy cố gắng sử dụng véc-tơ để tìm lời giải thay vì sử dụng vòng lặp khi có thể. Cùng câu hỏi như trên, chúng ta có thể cho lời giải đơn giản hơn bằng cách sử dụng hàm match().
vitri<-match(TRUE,trump_tweets$favorite_count>10^4) # vitri là chỉ số nhỏ nhất mà số like nhiều hơn 10.000
trump_tweets$created_at[vitri] # thời điểm viết câu tweet đó## [1] "2011-12-21 15:36:36 EST"
Khi làm việc với vòng lặp while những người mới làm quen với lập trình rất dễ rơi vào trạng thái vòng lặp vô hạn. Dưới đây là một ví dụ về một vòng lặp như vậy. Biến \(kiem\_tra\) nhận giá trị ban đầu là \(TRUE\) và trong các câu lệnh nằm trong vòng lặp không có câu lệnh nào tác động đến giá trị của biến đó. Bạn đọc sẽ thấy giá trị \(i\) được in ra tăng dần và không bao giờ dừng lại. Bạn đọc chỉ có thể dừng chương trình chạy bằng cách nhấn vào biểu tưởng “STOP” phía trên bên phải cửa sổ R console.
# HÃY CẨN THẬN VÌ ĐÂY LÀ VÒNG LẶP VÔ HẠN
kiem_tra<-TRUE
while (kiem_tra){ # kiem_tra luôn luôn nhận giá trị TRUE
print(paste0("Giá trị của i hiện tại: ", i)) # in ra màn hình phần tử thứ i
}Kinh nghiệm của chúng tôi khi sử dụng vòng lặp không xác định là luôn luôn sử dụng một biến, tạm gọi là \(i\), không liên quan đến chương trình chạy và được gán cho giá trị tăng dần trong vòng lặp. Trong biển thức điều kiện luôn luôn kèm thêm một điều kiện là \(i\) nhỏ hơn số lần lặp tối đa mà người lập trình quy định. Bạn đọc có thể quan sát đoạn lệnh sau:
loop_max<-10^4
i<-1 # i ban đầu là 1
kiem_tra<-TRUE
while (kiem_tra & (i<= loop_max)){
i<-i+1 # luôn luôn tăng i
print(paste0("Giá trị của i hiện tại: ", i)) # in ra màn hình phần tử thứ i
}Các đoạn câu lệnh kiểu trên sẽ lặp tối đa là 10.000 lần do chúng ta sử dụng thêm điều kiện (i<= loop_max)
1.4.2.3 Điều khiển vòng lặp
Khi bạn đọc viết các vòng lặp for hoặc while, R cung cấp các từ khóa để bạn đọc có thể điều khiển vòng lặp. Các từ khóa đó bao gồm break và next. Ý nghĩa của các từ khóa này như sau
| Từ khóa | Ý nghĩa |
|---|---|
| next | Chuyển tới bước lặp tiếp theo, bỏ qua các câu lệnh còn lại trong vòng lặp hiện tại |
| break | Dừng vòng lặp ngay lập tức |
Bạn đọc quan sát giá trị trả ra màn hình của đoạn câu lệnh sau đề hiểu cách sử dụng next trong vòng lặp
qua = c("chuối", "táo", "cam", "chanh") # Vec-tơ chứa tên các loại quả
for (ten in qua){ # cho biến ten nhận lần lượt các giá trị trong vec-tơ qua
if (ten == "cam"){
next # nếu ten là "cam" thì chuyển qua vòng lặp tiếp theo
}
print(ten) # in ten ra màn hình
} # kết thúc vòng lặp for## [1] "chuối"
## [1] "táo"
## [1] "chanh"
Có thể thấy rằng trong các loại quả được in ra màn hình không có giá trị \(cam\) bởi vì khi biến \(ten\) bằng giá trị này từ khóa next đã kết thúc vòng lặp hiện tại, bỏ qua dòng lệnh print() và đi đến vòng lặp tiếp theo. Vẫn các câu lệnh như trên nhưng thay next bằng break, chúng ta có thể quan sát R trả ra kết quả như sau
qua = c("chuối", "táo", "cam", "chanh") # Vec-tơ chứa tên các loại quả
for (ten in qua){ # cho biến ten nhận lần lượt các giá trị trong vec-tơ qua
if (ten == "cam"){
break # nếu ten là "cam" thì kết thúc vòng lặp ngay lập tức
}
print(ten) # in ten ra màn hình
} # kết thúc vòng lặp for## [1] "chuối"
## [1] "táo"
R chỉ trả ra tên hai loại quả là \(chuối\) và \(táo\) bởi vì khi gặp giá trị \(cam\) từ khóa break đã kết thúc vòng lặp for.
Trong R còn có một kiểu viết vòng lặp không xác định khác với vòng lặp while đó là viết vòng lặp sử dụng câu lệnh repeat. Khi sử dụng vòng lặp repeat bạn đọc luôn luôn phải sử dụng từ khóa break để kết thúc vòng lặp và tránh bị lặp vô hạn. Cách sử dụng repeat trong R như sau
qua = c("chuối", "táo", "cam", "chanh") # Vec-tơ chứa tên các loại quả
i<-0
repeat{
i<-i+1 # luôn luôn tăng i
print(qua[i]) # in tên ra màn hình
if (i== length(qua)){break}
} # kết thúc vòng lặp repeat## [1] "chuối"
## [1] "táo"
## [1] "cam"
## [1] "chanh"
Trong vòng lặp repeat ở trên, chúng tôi sử dụng điều kiện là \(i\) bằng độ dài của véc-tơ để kết thúc vòng lặp. Những bạn đọc mới làm quen với lập trình sẽ dễ bị nhầm lẫn về cách kết thúc vòng lặp của while và repeat. Cách hoạt động của hai vòng lặp này là tương đương nhau nên chúng tôi cho rằng những bạn đọc chưa quen với lập trình nên chỉ chọn một trong hai cách viết trong quá trình viết câu lệnh.
1.4.3 Viết hàm số
Hàm số có vai trò quan trọng trong R trong tất cả các ngôn ngữ lập trình nào khác. Hàm số đảm bảo sự chính xác và tiện lợi khi lập trình trình và hàm số là phương pháp chuyển giao kiến thức và kinh nghiệm hiệu quả từ người dùng này đến người dùng khác.
Hàm số đặc biệt có ý nghĩa khi bạn phải thực hiện một đoạn câu lệnh một cách lặp đi lặp lại và do sự thay đổi của một số yếu tố đầu vào. Thay vì phải làm đi làm lại công việc đó một cách thủ công, bạn hãy viết quy trình đó thành một hàm số.
Khi chúng ta muốn chuyển giao kinh nghiệm, kiến thức của mình cho một người khác, hãy viết chương trình của bạn dưới dạng hàm số và chuyển giao. Người dùng có thể không hiểu được ý nghĩa của chương trình của bạn ngay thì ít nhất cũng có thể sử dụng được kiến thức của bạn. Nếu bạn đọc để ý các thư viện cài đặt thêm trên R đều là tập hợp của các hàm số.
Hàm số trên R ngoài các hàm sẵn có ngay khi bạn cài R, các hàm số nằm trong các thư viện mà bạn đọc cài đặt bổ sung, và các hàm số mà bạn đọc tự định nghĩa.
1.4.3.1 Hàm số do người dùng tự định nghĩa.
Từ khóa để khai báo một hàm số là function(). Để tự tạo một hàm số tên là \(f\) nhận giá trị là \(x^2\) thì bạn đọc sử dụng đoạn câu lệnh như sau:
f<-function(x){ # là một hàm số của biến x
return(x^2) # trả lại giá trị của hàm số là x^2
}Thay vì sử dụng từ khóa return, bạn đọc cũng có thể sử dụng tên hàm số đển gán cho giá trị trả lại:
f<-function(x){ # là một hàm số của biến x
f<-x^2 # trả lại giá trị của hàm số là x^2
}Đôi khi bạn đọc sẽ gặp các đoạn lệnh khai báo hàm số không có từ khóa return và cũng không có phần gán giá trị cho hàm số. Khi đó R sẽ luôn luôn lấy giá trị được trả ra cuối cùng để gán giá trị cho hàm số đó.
f<-function(x) x^2Cách viết này chỉ phù hợp cho các hàm số ngắn gọn và chúng tôi khuyên bạn đọc hãy luôn sử dụng từ khóa return khi trả lại giá trị cho hàm số.
Sau khi đã chạy các đoạn lệnh khai báo hàm số \(f(x) = x^2\), R sẽ lưu đối tượng có tên \(f\) là kiểu hàm số lên môi trường làm việc chung. Để gọi hàm số và thực hiện tính toán, bạn đọc cần viết đúng tên hàm và cho tham số \(x\) giá trị phù hợp.
class(f) # kiểu của đối tượng f là function## [1] "function"
f(10) # cho tham số x giá trị bằng 10## [1] 100
Từ khóa return() được sử dụng để trả lại giá trị cho hàm số \(f\) và R sẽ gán giá trị cho hàm số \(f\) ngay lập tức khi gặp hàm câu lệnh \(return()\). Nếu trong đoạn câu lệnh của hàm số \(f\) có nhiều từ khóa return, giá trị của \(f\) sẽ được gán bằng từ khóa return đầu tiên. Hãy quan sát ví dụ sau:
f<-function(x){ # là một hàm số của biến x
return(x^2) # trả lại giá trị của hàm số là x^2 khi gặp return
return(x^3) # R sẽ không chạy câu lệnh này
}
f(10) # trả lại gái trị là 100Cách đặt tên hàm số cũng giống như đặt tên biến trong R, bạn đọc cần lựa chọn tên hợp lệ và tránh các từ khóa. Biến \(x\) trong phần khai báo hàm số ở trên được gọi là biến, tham số, hoặc tùy biến. Hàm số trong R có thể không có tham số nào hoặc có thể có rất nhiều tham số, mỗi tham số là một kiểu đối tượng khác nhau, việc này hoàn toàn tùy thuộc vào người lập trình. Bên trong dấu \(\{\}\) của từ khóa function() được gọi là môi trường cục bộ, R sẽ luôn ưu tiên biến nằm trong môi trường này trước tất cả các môi trường khác. Vấn đề sẽ được thảo luận ở phần tiếp theo. Một điểu cần lưu ý là khi viết hàm số hãy luôn luôn có tài liệu đi kèm rõ ràng để người sử dụng khác, hoặc chính mình khi sử dụng có thể hiểu hay nhớ được hàm số được sử dụng như thế nào và với mục đích gì.
Tham số hay biến số là phần thiết yếu của các hàm trong R. Trong phần tiếp theo, chúng ta sẽ xem xét cách tham số trong hàm số hoạt động như thế nào, chẳng hạn như cách tạo giá trị mặc định cho tham số, cách xử lý các giá trị tham số bị thiếu, cách bổ sung vào tham số bằng cách sử dụng dấu ba chấm \(...\).
Để tạo giá trị mặc định cho tham số bạn đọc cần tạo giá trị phù hợp khi khai báo hàm số. Tạo giá trị mặc định cho tham số là quan trọng khi bạn đọc viết các hàm số có nhiều tham số bởi vì khi bạn gọi hàm số và quên tạo giá trị cho một vài tham số nào đó, R sẽ sử dụng giá trị mặc định để tính toán. Hãy xem xét ví dụ sau: bạn muốn viết một hàm số để tính giá trị hiện tại (present value) của một dòng tiền được quan sát theo năm và được lưu trong một véc-tơ tên là \(CF\). Lãi suất tính theo kiểu lãi gộp là \(i\). Chúng ta sẽ sử dụng giá trị mặc định là \(5\%\) để gán cho \(i\)
PV<-function(i = 0.05, CF){ # Hàm số tính giá trị hiện tại của dòng tiền CF
n<-length(CF)
discount_factor<-(1+i)^(-(1:n))
return (sum(discount_factor * CF))
}Giả sử dòng tiền có giá trị là $1.000 tại thời điểm 1 và tăng dần $1000 mỗi năm và lên đến $10.000 tại năm thứ 10. Mức lãi suất gộp \(i = 10\%/năm\). Giá trị hiện tại của dòng tiền được tính bằng hàm \(PV\) như sau
MyCF<-seq(1000,10000,length=10)
PV(i = 0.1, MyCF) # Giá trị hiện tại của dòng tiền MyCF tại lãi suất 10%/năm## [1] 29035.91
Khi chúng ta quên không gán giá trị cho tham số \(i\) khi gọi hàm \(PV\), R sẽ cho \(i\) nhận giá trị mặc định là \(5\%\)
PV(CF = MyCF) # Giá trị hiện tại của dòng tiền MyCF tại lãi suất 5%/năm## [1] 39373.78
Sử dụng dấu ba chấm \(...\) khi khai báo tham số của một hàm số là phương pháp để người lập trình sử dụng tham số có sẵn của một hàm số khác. Nguyên tắc hoạt động của cách khai báo tham số này thể hiện qua ví dụ sau: hàm \(PV\) được xây dựng ở trên chỉ tính được dòng tiền tại các thời điểm cuối các năm. Bạn đọc muốn hàm \(PV\) có thể tính được giá trị hiện tại của dòng tiền trong cả hai trường hợp: dòng tiền bắt đầu từ thời điểm đầu năm hoặc dòng tiền bắt đầu từ cuối năm; bằng cách thêm vào một tham số \(bat\_dau\); khi \(bat\_dau = 0\) thì thời điểm bắt đầu là đầu năm thứ nhất và khi \(bat\_dau = 1\) thì thời điểm bắt đầu là cuối năm thứ nhất. Thay vì sửa lại làm \(PV\) chúng ta có thể viết một hàm mới, tạm gọi là \(PV1\) và sử dụng tham số của hàm \(PV\)
PV1<-function(bat_dau,i,...){ # chúng ta chỉ sử dụng tham số i của PV, các tham số khác khai báo bằng ...
if (bat_dau==1) {
return (PV(i,...)) # PV1 sử dụng các tham số còn lại của PV
} else {
return ( (1+i)*PV(i,...) ) # PV1 sử dụng các tham số còn lại của PV
}
}Khi gọi hàm \(PV1\) chúng ta cần gọi đầy đủ tham số:
PV1(bat_dau = 0,i = 0.1, CF = MyCF) # dòng tiền bắt đầu từ đầu năm thứ 1## [1] 31939.5
PV1(bat_dau = 1,i = 0.1, CF = MyCF) # dòng tiền bắt đầu từ cuối năm thứ 1## [1] 29035.91
Bạn đọc cũng có thể sử dụng cách mượn tham số này để sử dụng các hàm số có sẵn trong R. Trong ví dụ dưới đây, chúng tôi tự xây dựng một hàm có tên là myplot() để vẽ đồ thị rải điểm của một véc-tơ kiểu số \(x\) theo chỉ số của véc-tơ đó đồng thời và mượn các tham số \(main\), \(xlab\), \(ylab\) của hàm plot() có sẵn:
myplot<-function(x,...){ # hàm myplot vẽ đồ thị rải điểm
n<-length(x) # độ dài của véc-tơ x
plot(1:n,x,...)
}Chúng ta sẽ sử dụng hàm myplot() để vẽ đồ thị rải điểm của véc-tơ \(x\) nhận giá trị bằng véc-tơ kiểu chuỗi thời gian \(AirPassengers\).
myplot(AirPassengers,main="Số lượng hành khách các tháng",
ylab = "Số lượng hành khách", # tùy biến ylab của hàm plot()
xlab = "", # tùy biến xlab của hàm plot()
type = "l", color = "red") # tùy biến type và color của hàm plot
Bạn đọc có thể tham khảo cách sử dụng hàm plot() trong phần đồ thị cơ bản trong cuốn sách này.
1.4.3.2 Hàm số được xây dựng sẵn
Hàm số được xây dựng sẵn là các hàm số được phát triển sẵn trong R và các hàm số trong các thư viện mà bạn đọc cài đặt thêm cho R. Để biết R hiện đang có các thư viện nào đang sẵn sàng để sử dụng, bạn đọc chỉ cần sử dụng câu lệnh
search() # liệt kê danh sách các đối tượng, thư viện đang sẵn có theo thứ tự ưu tiên## [1] ".GlobalEnv" "package:dslabs" "package:ggrepel"
## [4] "package:pryr" "package:gridExtra" "package:grid"
## [7] "package:forcats" "package:ggplot2" "package:kableExtra"
## [10] "package:knitr" "package:dplyr" "package:readxl"
## [13] "package:stats" "package:graphics" "package:grDevices"
## [16] "package:utils" "package:datasets" "package:methods"
## [19] "Autoloads" "package:base"
Đa số các phiên bản R đều có sẵn các thư viện như \(stats\), \(graphics\), \(utils\),… Để biết cụ thể hơn trong một thư viện có những đối tượng (hàm số, nhóm các hàm số) nào khác, chúng ta sử dụng câu lệnh
library(help = "stats") # liệt kê các đối tượng trong thư viện statsBạn đọc sẽ thấy cửa sổ Script liệt kê ra danh sách các hàm số hoặc tên các đối tượng lưu chứa nhóm các hàm số đã được phát triển sẵn trong thư viện \(stats\). Một vài đối tượng được liệt kê ra trong danh sách là các hàm số: hàm AIC(), hàm ARMAacf,… Một số đối tượng là nhóm các hàm số, chẳng hạn như Beta hay Binomal. Khi bạn đọc thử gọi \(Beta\) trên cửa sổ Console sẽ gặp lỗi vì đó không phải là tên chính xác của hàm số. Thay vì thế hãy chạy câu lệnh \(? Beta\) để thấy rằng trong đối tượng \(Beta\) của thư viện \(stats\) có một nhóm các hàm số liên quan đến phân phối xác suất \(Beta\): hàm dbeta(), hàm pbeta(), hàm qbeta(), và hàm rbeta().
Chúng tôi không bàn đến việc làm thế nào để biết sử dụng hàm số nào trong một trường hợp cụ thể bởi vì đương nhiên không có câu trả lời chung cho câu hỏi này. Việc này tùy thuộc vào chuyên môn, hiểu biết, khả năng tìm kiếm của bạn đọc. Chúng tôi muốn tập trung vào việc đảm bảo bạn đọc gọi đúng hàm số mà bạn mong muốn. Sẽ không có vấn đề lớn nếu tên hàm số bạn cần gọi là duy nhất trên cửa số R bạn đang làm việc. Tuy nhiên, khi có một vài đối tượng khác có tên giống như tên hàm số bạn đang sử dụng, bạn sẽ gặp vấn đề.
Để làm được việc này bạn đọc nên hiểu một chút về môi trường làm việc và thứ tự ưu tiên khi gọi tên một đối tượng trong R. Khi bạn làm việc trên R, có ba môi trường mà R sử dụng để lưu trữ các đối tượng. Môi trường thứ nhất tạm gọi là môi trường chung (thuật ngữ công nghệ thông tin gọi là toàn cục), thứ hai là môi trường các thư viện, và cuối cùng là môi trường trong một hàm số cụ thể (thuật ngữ CNTT gọi là cục bộ). Khi bạn gọi tên một đối hay một hàm số, R sẽ luôn luôn ưu tiên theo thứ tự là: môi trường cục bộ \(\rightarrow\) môi trường chung (toàn cục) \(\rightarrow\) môi trường các thư viện. Do có nhiều thư viện cùng mở trên R nên để biết thứ tự ưu tiên của các thư viện bạn đọc sử dụng hàm search(). Các thư viện được ưu tiên hơn sẽ có chỉ số nhỏ hơn (xuất hiện) trước khi sử dụng search().
Nhìn chung các thư viện cài đặt thêm sẽ thường được ưu tiên hơn các thư viện có sẵn. Nếu một hàm trong thư viện cài đặt thêm trùng tên với một hàm trong thư viện có sẵn, R ưu tiên thư viện cài đặt thêm. Thật vậy, hàm số tên filter() là một hàm được xây dựng sẵn trong thư viện \(stats\). Tuy nhiên trong thư viện \(dplyr\) cũng có một hàm tên là filter(). Trước khi gọi thư viện \(dplyr\), mỗi khi bạn đọc gọi hàm filter(), R sẽ luôn hiểu đây là hàm filter() của thư viện \(stats\).
? filter # nếu chưa gọi thư viện dplyr, filter là hàm của thư viện statsSau khi chúng ta gọi thư viện \(dplyr\), chúng ta sẽ thấy thư viện \(dplyr\) xuất hiện trước thư viện \(stats\) theo thứ tự ưu tiên.
library(dplyr) # gọi thư viện dplyr
search() # sau khi gọi thư viện dplyr, thư viện này được ưu tiên trước statsTrong thư viện \(dplyr\) cũng có một hàm tên là filter(). Theo thứ tự ưu tiên nếu bạn đọc gọi hàm filter() thì R sẽ hiểu đây là hàm filter của thư viện \(dplyr\). Lúc này muốn sử dụng hàm filter() của thư viện \(stats\) bạn đọc cần phải sử dụng tên thư viện viết trước hàm này stats::filter().
Như đã nói ở phần trước, môi trường chung cũng là môi trường được ưu tiên trước môi trường các thư viện. Bạn đọc có thể thấy từ kết quả hàm search(), môi trường chung, ký hiệu \(.GlobalEnv\), luôn xuất hiện trước tiên. Môi trường chung chính là nơi lưu trữ tất cả các hàm số hay đối tượng mà bạn đọc tự định nghĩa. Môi trường chung luôn được ưu tiên trước môi trường thư viện. Điều này có nghĩa là nếu bạn đọc tự định nghĩa một biến, một véc-tơ, hay hàm số có tên là \(filter\), R sẽ ưu tiên tên \(filter\) cho đối tượng mà bạn đọc tự định nghĩa. Như vậy, nếu bạn đọc sử dụng tên \(filter\) cho một hàm bạn tự định nghĩa, bạn sẽ cần phải sử dụng thêm tên thư viện để gọi hàm filter() từ các thư viện \(dplyr\) hoặc \(stats\).
Còn một môi trường khác, tạm gọi là môi trường cục bộ, sẽ được ưu tiên hơn môi trường chung. Môi trường cục bộ mô tả môi trường bên trong một hàm số mà bạn đọc tự định nghĩa. Giả sử sau khi bạn đọc tự định nghĩa một hàm filter() trên môi trường chung và sau đó tự định nghĩa một hàm số \(f\) có sử dụng một tham số (có thể là biến hoặc hàm số) có tên là \(filter\) thì mỗi khi bạn đọc gọi hàm số \(f\), đối tượng tên \(filter\) sẽ luôn được hiểu là tham số của hàm số \(f\). Môi trường bên trong hàm \(f\) được gọi là môi trường cục bộ. Bạn đọc hãy quan sát ví dụ dưới đây để hiểu hơn về môi trường chung và môi trường cục bộ
filter<-function(){return(pi)} #Tự định nghĩa hàm filter trong môi trường chung
filter() # hàm filter trong .GlobalEnv luôn bằng pi ## [1] 3.141593
Trước hết chúng ta định nghĩa một hàm tên là \(filter()\) trong môi trường chung luôn nhận giá trị bằng hằng số \(\pi\). Lúc này khi chúng ta gọi filter(), R sẽ hiểu rằng đây là hàm filter() do chúng ta tự định nghĩa. Sau đó chúng ta định nghĩa một hàm số tên là \(f\) đồng thời bên trong hàm \(f\) chúng ta định nghĩa một hàm filter() khác nhận giá trị là 10. Hàm filter() bên trong hàm f được gọi là hàm số trong môi trường cục bộ.
f<-function(){
filter<-function(){return(10)} # bên trong hàm f, định nghĩa lại hàm filter bằng 10
return(filter())
}Khi chúng ta gọi hàm số f, hàm số này lại gọi một hàm số tên là filter được định nghĩa bên trong hàm số nó. Bởi vì R ưu tiên môi trường cục bộ trước môi trường hàm \(filter\) bên trong f có giá trị bằng 10. Bên ngoài hàm số f, chúng ta lại gọi filter() thì giá trị trả lại là \(\pi\) vì đây là môi trường chung.
f() # trả lại giá trị là 10 vì hàm filter bên trong f nhận giá trị 10## [1] 10
filter() # trả lại giá trị pi## [1] 3.141593
Tất cả các hàm số mà bạn đọc thường xuyên sử dụng hãy lưu trong các file và mỗi khi cần sử dụng bạn đọc chỉ cần gọi tên file đó thay vì copy toàn bộ các câu lệnh của các hàm số vào cửa sổ Script. Hàm số để gọi một file lên cửa sổ R bạn đang sử dụng là hàm source(). Chẳng hạn như tất cả các hàm số bạn đọc tự định nghía được lưu ở một file có tên là “myfunctions.R”, bạn chỉ cần sử dụng câu lệnh sau để gọi tất cả các hàm số lên cửa sổ đang làm việc:
source("Đường dẫn đến file/myfunction.R")2 Ma trận, mảng nhiều chiều và \(list\)
Trong cuốn sách này chúng tôi cố gắng tránh nhắc đến các khái niệm toán học phức tạp bởi đối tượng chúng tôi hướng đến là những người làm việc với dữ liệu nhưng không có một nền tảng chuyên sâu về toán học. Tuy nhiên để làm việc được với dữ liệu thì các kiến thức về ma trận nói riêng và kiến thức về đại số tuyến tính nói chung là bắt buộc phải nắm vững. Điều đáng tiếc là tại thời điểm chúng tôi viết cuốn sách này, đa số các chương trình đào tạo dành cho sinh viên các ngành kinh tế đang cắt giảm dần kiến thức về toán học và đặc biệt là kiến thức đại số tuyến tính.
2.1 Ma trận
Ma trận có ý nghĩa đặc biệt quan trọng trong phân tích dữ liệu bởi đa số các dữ liệu đều được chuyển thành kiểu ma trận để dễ dàng phân tích và tính toán. Cũng giống như véc-tơ, ma trận là một đối tượng dùng để lưu các biến có cùng kiểu. Khác với véc-tơ, ma trận lưu phần tử theo hàng và cột, nghĩa là trong không gian hai chiều trong khi véc-tơ lưu phần tử trong không gian một chiều. Bạn đọc cũng có thể hiểu véc-tơ là một cột trong khi ma trận là tập hợp của các cột có cùng độ dài. Kích thước của một véc-tơ là chiều dài của véc-tơ đó trong khi kích thước của một ma trận là số hàng và số cột của ma trận đó.
2.1.1 Khởi tạo ma trận
Hàm số dùng để tạo ra ma trận trong R là hàm matrix(). Khi tạo ma trận, bạn đọc sẽ luôn luôn phải khởi tạo giá trị cho ma trận đó. Đoạn lệnh sau sẽ khởi tạo một ma trận có tên là \(M\), có 3 hàng, 4 cột, và giá trị trong ma trận là các số tự nhiên từ 1 đến 12 được sắp xếp theo thứ tự
M<-matrix(1:12, nrow = 3, ncol = 4) # nrow: số hàng, ncol: số cột
M # in M ra của sổ console## [,1] [,2] [,3] [,4]
## [1,] 1 4 7 10
## [2,] 2 5 8 11
## [3,] 3 6 9 12
Các giá trị dùng để khởi tạo cho ma trận là các số từ 1 đến 12 và được điền vào ma trận \(M\) theo nguyên tắc từ trên xuống dưới rồi từ trái sang phải, nghĩa là cột thứ nhất sẽ được ưu tiên cho giá trị trước; phần tử hàng thứ nhất của cột thứ nhất sẽ được điền giá trị trước, sau đó đến phần tử ở hàng thứ hai của cột thứ nhất, …; sau khi hết cột thứ nhất R sẽ tiếp tục điền vào giá trị ở hàng thứ nhất của cột thứ hai,…, và cứ tiếp tục như thế sau khi tất cả các phần tử trong ma trận đều có giá trị. Véc-tơ dùng đề khởi tạo giá trị cho ma trận có độ dài 12 vừa đúng với số phần tử trong ma trận nên câu lệnh tạo ma trận \(M\) ở trên hoạt động bình thường. Trong trường hợp bạn đọc sử dụng véc-tơ có độ dài khác 12 để khởi tạo giá trị cho ma trận, câu lệnh vẫn sẽ chạy nhưng có kèm theo cảnh báo:
M<-matrix(1:13, nrow = 3, ncol = 4) # Code chạy kèm theo cảnh báo;
M<-matrix(1:5, nrow = 3, ncol = 4) # Code chạy kèm theo cảnh báo; Bạn đọc có thể thấy rằng:
Nếu véc-tơ dùng để khởi tạo giá trị cho ma trận \(M\) có độ dài lớn hơn 12, R sẽ dùng 12 giá trị đầu tiên để khởi tạo giá trị cho ma trận.
Nếu véc-tơ dùng để khởi tạo giá trị cho ma trận \(M\) có độ dài nhỏ hơn 12, R sẽ lặp lại véc-tơ đó cho đến khi có độ dài lớn hơn hoặc bằng 12 rồi sau đó dùng 12 giá trị đầu tiên để khởi tạo giá trị cho ma trận.
Khi khởi tạo ma trận, bạn đọc có thể yêu cầu giá trị được khởi tạo theo hàng thay vì theo cột bằng tùy biến \(byrow = TRUE\) trong hàm matrix().
M<-matrix(1:12, nrow = 3, ncol = 4, byrow = TRUE) # nrow: số hàng, ncol: số cột
M # in M ra của sổ console## [,1] [,2] [,3] [,4]
## [1,] 1 2 3 4
## [2,] 5 6 7 8
## [3,] 9 10 11 12
Để biết kích cỡ của ma trận, chúng ta sử dụng hàm dim(). Hàm dim() trên ma trận sẽ trả lại giá trị là một véc-tơ kiểu số có độ dài là hai, phần tử thứ nhất là số hàng, phần tử thứ hai là số cột của ma trận
dim(M) # ma trận 3 hàng 4 cột## [1] 3 4
Ma trận cũng có thể được khởi tạo bằng cách ghép các véc-tơ hoặc các ma trận khác theo hàng hay theo cột bằng các hàm cbind() hoặc rbind():
Hàm
cbind()nối các ma trận có cùng số hàng hoặc ma trận với véc-tơ có độ dài bằng số hàng của ma trận.Tương tự,
rbind()nối các ma trận có cùng số cột hoặc ma trận với véc-tơ có độ dài bằng số cột của ma trận.
## [,1] [,2] [,3] [,4] [,5]
## [1,] 1 2 3 4 1
## [2,] 5 6 7 8 1
## [3,] 9 10 11 12 1
## [,1] [,2] [,3] [,4]
## [1,] 1 2 3 4
## [2,] 5 6 7 8
## [3,] 9 10 11 12
## [4,] 1 1 1 1
Các phép tính toán thông thường trên ma trận cũng có nguyên tắc giống như đối với véc-tơ. Các phép toán như cộng, trừ, nhân, chia, lũy thừa, …, sẽ tác động lên tất cả các phần tử trong ma trận theo thứ tự của các phần tử xuất hiện trên ma trận. Bạn đọc hãy quan sát kết quả của các ví dụ sau để hiểu cách R thực hiện các phép toán trên ma trận
Nhân ma trận \(M\) kích thước \(3 \times 4\) với một số
M<-matrix(1:12, nrow = 3, ncol = 4)
M * 2 # in M*2 ra của sổ console## [,1] [,2] [,3] [,4]
## [1,] 2 8 14 20
## [2,] 4 10 16 22
## [3,] 6 12 18 24
Bạn đọc có thể thấy rằng kết quả nhận được là một ma trận có kích thước bằng với kích thước của ma trận \(M\) và mỗi phần tử bằng phần tử ở vị trí tương ứng của ma trận \(M\) nhân với 2.
Khi thực hiện phép nhân thông thường ma trận \(M\) với một ma trận \(M_1\) có cùng kích thước thì kết quả nhận được là một ma trận mà mỗi phần tử bằng tích của 2 phần tử ở vị trí tương ứng của \(M\) và \(M_1\). R sẽ báo lỗi nếu thực hiện phép nhân thông thường giữa hai ma trận không có cùng kích thước.
M<-matrix(1:12, nrow = 3, ncol = 4)
M1<-matrix(rep(c(0,1),6), nrow = 3, ncol = 4)
M * M1# in M * M1 ra của sổ console## [,1] [,2] [,3] [,4]
## [1,] 0 4 0 10
## [2,] 2 0 8 0
## [3,] 0 6 0 12
Phép nhân thông thường cũng có thể được thực hiện giữa ma trận \(M\) với một véc-tơ có độ dài nhỏ hơn hoặc bằng số phần tử của \(M\). Trước khi thực hiệp phép nhân, R sẽ chuyển các phần tử trong véc-tơ vào một ma trận có kích thước tương ứng với \(M\) sau đó thực hiện phép nhân giống như nhân hai ma trận có cùng kích thước:
M<-matrix(1:12, nrow = 3, ncol = 4)
x<-c(-2,-1,0,1,2) # véc-tơ độ dài 5
M * x # phép nhân được thực hiện mà không báo lỗi## [,1] [,2] [,3] [,4]
## [1,] -2 4 -7 20
## [2,] -2 10 0 -22
## [3,] 0 -12 9 -12
Khi thực hiện tính toán như trên, R đã tự động lặp lại \(x\) cho đến khi số lượng phần tử bằng với số phần tử của \(M\), điền các giá trị này vào một ma trận có kích thước bằng với kích thước của \(M\) rồi sau đó thực hiện phép nhân. Thật vậy
y<-rep(x,3) # lặp lại x cho đến khi số phần tử của véc-tơ thu được lớn hơn 12
M1<-matrix(y[1:12],nrow = 3, ncol = 4) # dùng 12 giá trị ban đầu để tạo thành ma trận có cùng kích thước 3*4
M * M1 # kết quả giống như M * x## [,1] [,2] [,3] [,4]
## [1,] -2 4 -7 20
## [2,] -2 10 0 -22
## [3,] 0 -12 9 -12
2.1.2 Cách lấy phần tử con và ma trận con của ma trận.
Tương tự như với véc-tơ, chúng ta sử dụng \([]\) để lấy phần tử con trong ma trận. Khác với véc-tơ, ma trận có chỉ số hàng và chỉ số cột nên chúng ta cần cho biết phần tử được lấy ra ở hàng thứ bao nhiêu và cột thứ bao nhiêu:
M[2,3] # phần tử ở hàng thứ hai, cột thứ ba của ma trận M## [1] 8
Chúng ta cũng có thể lấy ra véc-tơ hàng hoặc véc-tơ cột của ma trận bằng cách sau
M[,3] # lấy ra véc-tơ cột thứ 3 của ma trận M## [1] 7 8 9
M[2,] # lấy ra véc-tơ hàng thứ 2 của ma trận M## [1] 2 5 8 11
Để lấy ra một ma trận con của một ma trận, chúng ta cũng tạo véc-tơ chỉ số giống như cách tạo chỉ số với véc-tơ. Thay vì chỉ tạo một véc-tơ chỉ số duy nhất như khi làm với véc-tơ, chúng ta cần tạo một véc-tơ chỉ số theo hàng và một véc-tơ chỉ số theo cột. Bạn đọc có thể tạo chỉ số bằng một véc-tơ kiểu số hoặc véc-tơ kiểu logical hoặc kết hợp cả hai phương pháp này
chi_so_hang<-c(TRUE,FALSE,TRUE) # Chỉ số theo hàng kiểu logical
chi_so_cot<-c(2,4) # chỉ số cột theo kiểu số
M[chi_so_hang,chi_so_cot] # ma trận con của ma trận M## [,1] [,2]
## [1,] 4 10
## [2,] 6 12
2.1.3 Các phép toán trên ma trận.
Các phép toán trên ma trận có ý nghĩa đặc biệt trong phân tích dữ liệu. Ở các chương sách tiếp theo bạn đọc sẽ thấy rằng tất cả các tính toán nhằm biến đổi dữ liệu, hoặc ước lượng tham số cho các mô hình trên dữ liệu đều dựa trên các phép tính toán trên ma trận. Chúng tôi sẽ giải thích các phép toán này một cách đơn giản nhất để những bạn đọc không có nền tảng chuyên sâu về toán cũng có thể hiểu được. Tuy nhiên, để có kỹ năng thành thạo trong biến đổi dữ liệu, phân tích dữ liệu, và xây dựng các mô hình trên dữ liệu, chúng tôi khuyên bạn đọc nên tự trang bị cho mình các kiến thức về đại số tuyến tính và giải tích véc-tơ.
2.1.3.1 Phép chuyển vị.
Phép toán chuyển vị (transpose) là một phép toán biến đổi một ma trận \(M\) kích thước \(n \times p\) (\(n\) hàng và \(p\) cột) thành một ma trận mới, ký hiệu là \(M^T\), có kích thước \(p \times n\) (\(p\) hàng và \(n\) cột), đồng thời phần tử hàng thứ \(j\) và cột thứ \(i\) của ma trận \(M^T\) bằng phần tử ở hàng thứ \(i\) và cột thứ \(j\) của ma trận \(M\).
\[\begin{align} \begin{bmatrix} m_{11} & m_{12} & m_{13}\\ m_{21} & m_{22} & m_{23} \end{bmatrix} \xrightarrow[\text{(transpose)}]{\text{chuyển vị}} \begin{bmatrix} m_{11} & m_{21} \\ m_{12} & m_{22} \\ m_{13} & m_{23} \end{bmatrix} \end{align}\]
Hàm số để thực hiện phép chuyển vị ma trận trong R là hàm t()
M<-matrix(1:12, nrow = 3, ncol = 4, byrow = TRUE) # nrow: số hàng, ncol: số cột
M # in M ra của sổ console## [,1] [,2] [,3] [,4]
## [1,] 1 2 3 4
## [2,] 5 6 7 8
## [3,] 9 10 11 12
t(M) # ma trận chuyển vị của ma trận ## [,1] [,2] [,3]
## [1,] 1 5 9
## [2,] 2 6 10
## [3,] 3 7 11
## [4,] 4 8 12
Bạn đọc có thể thấy rằng nếu thực hiện phép chuyển vị hai lần liên tiếp ta sẽ thu được ma trận ban đầu
## [,1] [,2] [,3] [,4]
## [1,] 1 2 3 4
## [2,] 5 6 7 8
## [3,] 9 10 11 12
2.1.4 Phép nhân ma trận
Phép nhân ma trận (matrix multiplication) của ma trận \(A\) với ma trận \(B\) chỉ thực hiện được nếu số cột của ma trận \(A\) bằng với số hàng của ma trận \(B\). Giả sử rằng \(A\) có kích thước là \(n \times p\) và \(B\) có kích thước là \(p \times k\) thì kết quả của phép nhân ma trận của ma trận \(A\) với ma trận \(B\) là một ma trận \(M\) có kích thước \(n \times k\), phần tử ở hàng thứ \(i\) và cột thứ \(j\) của ma trận \(M\) là tích vô hướng giữa véc-tơ hàng \(i\) của ma trận \(A\) và véc-tơ cột \(j\) của ma trận \(B\). Nhắc lại với bạn đọc rằng tích vô hướng của hai véc-tơ \(x\) và \(y\) (phải) có cùng độ dài \(n\) được ký hiệu là \(<x,y>\) và được tính như sau
\[\begin{align} <x,y> = \sum\limits_{i = 1}^n \ x_i y_i \end{align}\]
trong đó \(x_i\), \(y_i\) lần lượt là phần tử thứ \(i\) của véc-tơ \(x\) và véc-tơ \(y\). Để phân biệt với phép nhân thông thường, chúng tôi sử dụng ký hiệu \(*\) cho phép nhân ma trận mà chỉ đơn giản ký hiệu phép nhân ma trận giữa ma trận \(A\) và ma trận \(B\) là \(AB\). Công thức dưới đây mô tả phép nhân ma trận giữa một ma trận có 2 hàng và 3 cột với một ma trận có 3 hàng và 4 cột để được một ma trận có kích thước là 2 hàng và 4 cột
\[\begin{align} && AB = M \\ && \begin{bmatrix} a_{11} & a_{12} & a_{13}\\ a_{21} & a_{22} & a_{23} \end{bmatrix} \% * \% \begin{bmatrix} b_{11} & b_{12} & b_{13} & b_{14} \\ b_{21} & b_{22} & b_{23} & b_{24} \\ b_{31} & b_{32} & b_{33} & b_{34} \end{bmatrix} = \begin{bmatrix} m_{11} & m_{12} & m_{13} & m_{14} \\ m_{21} & m_{22} & m_{23} & m_{24} \\ \end{bmatrix} \\ && m_{ij} = <A[i,],B[,j]> \end{align}\] trong đó \(A[i,]\) là véc-tơ hàng \(i\) của ma trận \(A\) và \(B[,j]\) là véc-tơ cột \(j\) của ma trận \(B\).
Toán tử dùng để thực hiện phép nhân ma trận trong R là \(\% * \%\). Bạn đọc có thể thực hiện phép nhân hai ma trận \(A\) và \(B\) như sau
## [,1] [,2] [,3]
## [1,] 1 3 5
## [2,] 2 4 6
## [,1] [,2] [,3] [,4]
## [1,] 1 4 7 10
## [2,] 2 5 8 11
## [3,] 3 6 9 12
## [,1] [,2] [,3] [,4]
## [1,] 22 49 76 103
## [2,] 28 64 100 136
Chúng ta có thể kiểm tra giá trị của phần tử ở hàng thứ 2 và cột thứ 3 của ma trận kết quả (số 100) chính là tích vô hướng giữa véc-tơ hàng thứ hai của ma trận \(A\) và véc-tơ cột thứ ba của ma trận \(B\).
\[\begin{align} <(2,4,6),(7,8,9)> &=& 2 \times 7 + 4 \times 8 + 6 \times 9 \\ &=& 100 \end{align}\]
Bạn đọc cần phân biệt giữa phép nhân ma trận (ký hiệu \(\% * \%\)) và phép nhân thông thường (ký hiệu \(*\)) như đã trình bày ở trên. Để tránh gây nhầm lẫn, chúng tôi luôn sử dụng cụm từ nhân ma trận cho phép nhân \(\% * \%\).
2.1.5 Ma trận đường chéo và ma trận đơn vị.
Các khái niệm và phép toán trên ma trận được trình bày từ phần này sẽ chỉ áp dụng trên ma trận vuông, nghĩa là ma trận có số hàng bằng với số cột. Trong một ma trận vuông, các phần tử nằm trên đường chéo \(chính\) là các phần tử có chỉ số hàng bằng với chỉ số cột, các phần tử nằm trên đường chéo \(phụ\) là các phần tử có chỉ số hàng cộng với chỉ số cột bằng \((n+1)\) trong đó \(n\) là số hàng. Với ma trận vuông \(M\) có kích thước \(n \times n\) đường chéo chính của ma trận được ký hiệu là \(diag(M)\) xác định như sau
\[\begin{align} M = \begin{bmatrix} m_{11} & m_{12} & m_{13} & m_{14} \\ m_{21} & m_{22} & m_{23} & m_{24} \\ m_{31} & m_{32} & m_{33} & m_{34}\\ m_{41} & m_{42} & m_{43} & m_{44} \end{bmatrix} \\ diag(M) = (m_{11}, m_{22}, m_{33}, m_{44}) \end{align}\]
Ma trận có tất cả các phần tử nằm \(ngoài\) đường chéo chính bằng 0 được gọi là ma trận đường chéo. Hàm diag() trong R được sử dụng để lấy ra véc-tơ đường chéo chính của một ma trận vuông và để tạo ra một ma trận đường chéo hoặc cũng có thể dùng để khai báo một ma trận đường chéo.
## [,1] [,2] [,3]
## [1,] 1 4 7
## [2,] 2 5 8
## [3,] 3 6 9
diag(M) # lấy ra véc-tơ đường chéo chính của M## [1] 1 5 9
## [,1] [,2] [,3]
## [1,] 1 0 0
## [2,] 0 10 0
## [3,] 0 0 100
Ma trận đơn vị kích thước \(n \times n\), thường được ký hiệu \(I_n\), là một ma trận đường chéo mà tất cả các phần tử trên đường chéo chính bằng 1. Ma trận đơn vị \(I_n\) có tính chất quan trọng là mọi ma trận \(M\) kích thước \(k \times n\) khi thực hiện phép nhân ma trận với ma trận \(I_n\) sẽ được kết quả đúng bằng ma trận \(M\). Bạn đọc có thể quan sát ví dụ sau
## [,1] [,2] [,3] [,4]
## [1,] 1 0 0 0
## [2,] 0 1 0 0
## [3,] 0 0 1 0
## [4,] 0 0 0 1
M<-matrix(1:20, nrow = 5, ncol = 4) # ma trận vuông 5 * 4
print(M %*% In) # kết quả vẫn là ma trận M## [,1] [,2] [,3] [,4]
## [1,] 1 6 11 16
## [2,] 2 7 12 17
## [3,] 3 8 13 18
## [4,] 4 9 14 19
## [5,] 5 10 15 20
2.1.6 Định thức của ma trận
Định thức của ma trận là một khái niệm toán học phức tạp. Phần lớn bạn đọc khi làm quen với khái niệm định thức trong môn học đại số tuyến tính sẽ được giới thiệu về công thức tính định thức của một ma trận, hoặc sử dụng định thức của ma trận để thực hiện tính toán như giải hệ phương trình thay vì thực sự hiểu khái niệm định thức được bắt đầu từ đâu. Định thức là một giá trị số thực đặc trưng của một ma trận vuông. Định thức cho biết nhiều tính chất quan trọng của ma trận đó, đồng thời định thức xuất hiện trong rất nhiều tính toán liên quan đến ma trận.
Hãy bắt đầu với một ma trận vuông \(M\) kích thước \(2 \times 2\) như sau: \[\begin{align} M = \begin{bmatrix} m_{11} & m_{12}\\ m_{21} & m_{22} \end{bmatrix} \end{align}\]
Định thức của ma trận \(M\) được ký hiệu là \(|M|\) hoặc \(det(M)\) được xác định bởi công thức \[\begin{align} det(M) = m_{11} \times m_{22} - m_{12} \times m_{21} \end{align}\]
Định thức của ma trận \(M\) có thể biểu diễn dưới dạng diện tích của hình bình hành tạo thành từ 4 điểm có tọa độ \((0,0)\), \((m_{11},m_{12})\), \((m_{21},m_{22})\), và \((m_{11}+m_{21}, m_{12}+m_{22})\). Thật vậy, giả sử hai ma trận \(M\) và \(M_1\) có kích thước \(2 \times 2\); ma trận \(M_1\) nhận được bằng cách đổi vị trí 2 dòng của ma trận \(M\)
\[\begin{align} M = \begin{bmatrix} 2 & 1 \\ 1 & 3 \end{bmatrix} \text{ và } M_1 = \begin{bmatrix} 1 & 3 \\ 2 & 1 \end{bmatrix} \\ det(M) = 5 \text{ và } det(M_1) = -5 \end{align}\]
Chúng ta có thể biểu diễn định thức của \(M\) và \(M_1\) qua diện tích của các hình bình hành như hình vẽ dưới đây:

Giá trị của định thức sẽ cho ta biết thông tin về các véc-tơ tạo nên ma trận:
Định thức bằng 0 chỉ xảy ra khi hai véc-tơ (hai mũi tên màu xanh và màu đỏ xuất phát từ điểm \((0,0)\)) trùng nhau hoặc đối đỉnh nhau. Điều này chỉ xảy ra khi véc-tơ thứ nhất bằng véc-tơ thứ hai nhân với một số. Trong trường hợp tổng quát với ma trận vuông kích thước \(n \times n\), định thức bằng 0 khi một véc-tơ nào đó là tổ hợp tuyến tính của các véc-tơ còn lại.
Định thức gần bằng 0 nghĩa là góc tạo bởi 2 véc-tơ rất gần 0 hoặc tạo với nhau một góc xấp xỉ 180 độ. Ma trận vuông kích thước \(n \times n\) có định thức xấp xỉ bằng 0 nghĩa là mối liên hệ tuyến tính giữa các véc-tơ của ma trận là rất chặt chẽ.
Dấu của định thức cho ta biết vị trí của các véc-tơ. Véc-tơ hàng thứ nhất tương ứng với màu xanh dương trong khi véc-tơ hàng thứ hai tương ứng với màu đỏ. Dấu của định thức dương chỉ khi véc-tơ màu xanh dương nằm phía trên (bên trái) véc-tơ màu đỏ, và dấu của định thức là âm chỉ khi véc-tơ màu xanh dương nằm phía dưới (bên phải) véc-tơ màu đỏ.
Với các ma trận vuông kích thước \(n \times n\); \(n \geq 3\) định thức của ma trận được tính bằng cách lựa chọn một dòng (hoặc cột) thứ \(i\) bất kỳ và sau đó thực hiện phép khai triển \[\begin{align} det(M) = \sum\limits_{j = 1}^n (-1)^{i+j} \times m_{ij} \times det(M_{-i,-j}) \end{align}\] trong đó \(M_{-i,-j}\) mà ma trận vuông kích thước \((n-1) \times (n-1)\) nhận được sau khi bỏ đi hàng thứ \(i\) và cột thứ \(j\) của ma trận \(M\).
Định thức của ma trận \(M\) kích thước \(3 \times 3\) có thể tính toán dựa trên định thức của các ma trận con và lựa chọn hàng \(i=2\) như sau
\[\begin{align} & & M = \begin{bmatrix} m_{11} & m_{12} & m_{13} \\ m_{21} & m_{22} & m_{23} \\ m_{31} & m_{32} & m_{33} \end{bmatrix} \\ & & det(M) = - m_{21} \times \begin{vmatrix} m_{12} & m_{13} \\ m_{32} & m_{33} \end{vmatrix} + m_{22} \times \begin{vmatrix} m_{11} & m_{13} \\ m_{31} & m_{33} \end{vmatrix} - m_{23} \times \begin{vmatrix} m_{11} & m_{12} \\ m_{31} & m_{32} \end{vmatrix} \end{align}\]
Hàm det() trong R được sử dụng để tính định thức của ma trận.
## [1] 5
Một vài tính chất quan trọng của định thức:
- Định thức của một ma trận đường chéo bằng tích các phần tử nằm trên đường chéo chính của ma trận đó. Ma trận đường chéo là một trường hợp đặc biệt của ma trận tam giác. Ma trận tam giác trên là ma trận có tất cả các phần tử nằm phía dưới đường chéo chính nhận giá trị bằng 0. Tương tự, ma trận tâm giác dưới là ma trận có tất cả các phần tử nằm phía trên đường chéo chính nhận giá trị bằng 0. Các ma trận tam giác có tính chất như đã phát biểu ở trên: định thức của các ma trận này bằng tích các phần tử nằm trên đường chéo chính.
M<-matrix(1:16,nrow = 4,ncol = 4)
M[lower.tri(M)]<-0 # cho các phần tử phía dưới đường chéo chính bằng 0
print(M) # ma trận M là ma trận tam giác trên## [,1] [,2] [,3] [,4]
## [1,] 1 5 9 13
## [2,] 0 6 10 14
## [3,] 0 0 11 15
## [4,] 0 0 0 16
## [1] 1056 1056
- Định thức của ma trận chuyển vị bằng định thức của ma trận ban đầu \[\begin{align} det(M) = det(M^T) \end{align}\]
## [1] -4.377982 -4.377982
- Định thức của tích hai ma trận bằng tích của các định thức. \[\begin{align} det(A \% * \% B) = det(A) \times det(B) \end{align}\]
M<-matrix(rnorm(16),nrow = 4,ncol = 4)
M1<-matrix(rnorm(16),nrow = 4,ncol = 4)
print(c(det(M%*%M1),det(M)*det(M1))) ## [1] 0.09626011 0.09626011
2.1.7 Ma trận nghịch đảo
Ma trận nghịch đảo của một ma trận vuông \(M\), thường được ký hiệu \(M^{-1}\), là ma trận vuông có cùng kích thước với ma trận \(M\) và thỏa mãn tính chất: phép nhân ma trận giữa ma trận \(M\) với ma trận nghịch đảo \(M^{-1}\) sẽ cho kết quả là một ma trận đơn vị. Không phải ma trận vuông nào cũng có ma trận nghịch đảo; chỉ có các ma trận có định thức khác là có ma trận nghịch đảo. Các ma trận có ma trận nghịch đảo được còn được gọi là các ma trận khả nghịch. Các ma trận khả nghịch luôn có một ma trận nghịch đảo duy nhất.
\[\begin{align} M \% * \% M^{-1} = I_n \end{align}\]
Hai lần lấy nghịch đảo liên tiếp với một ma trận khả nghịch sẽ quay trở lại ma trận ban đầu, hay nói một cách khác ma trận nghịch đảo của ma trận \(M^{-1}\) chính là ma trận \(M\) \[\begin{align} M^{-1} \% * \% M = I_n \end{align}\]
Phương pháp chung để tính toán ma trận nghịch đảo là dựa trên các ma trận liên hợp (adjugate matrix). Ma trận liên hợp của ma trận \(M\) được ký hiệu là \(adj(M)\) là ma trận vuông kích thước \(n \times n\) mà phần tử ở hàng thứ \(i\), cột thứ \(j\) được tính bằng \[\begin{align} adj(M)_{ij} = (-1)^{j+i} det(M_{-j,-i}) \end{align}\] Khi tính toán định thức của ma trận \(M\), chúng tôi đã sử dụng ký hiệu \(M_{-i,-j}\) cho ma trận vuông kích thước \((n-1) \times (n-1)\) nhận được sau khi bỏ đi hàng thứ \(i\) và cột thứ \(j\) của ma trận \(M\). Bạn đọc lưu ý rằng có sự thay đổi vị trí của \(i\) và \(j\) trong vế phải của phương trình ở trên. Ma trận nghịch đảo \(M^{-1}\) được tính từ ma trận liên hợp như sau \[\begin{align} M^{-1} = \cfrac{1}{det(M)} adj(M)_{ij} \end{align}\]
Nhìn chung, để tính toán ma trận nghịch đảo của một ma trận kích thước \(n \times n\), chúng ta sẽ phải tính toán định thức của ma trận ban đầu và định thức của \(n^2\) ma trận vuông có kích thước \((n-1) \times (n-1)\). Với các ma trận vuông có kích thước lớn, việc tính toán sử dụng công thức như trên sẽ tốn nhiều thời gian và bộ nhớ. Có nhiều thuật toán để tính xấp xỉ ma trận nghịch đảo của một ma trận. Trình bày các thuật toán này ở đây là không cần thiết. R sử dụng hàm solve() để tính toán ma trận nghịch đảo.
M<-matrix(rnorm(16),nrow = 4,ncol = 4)
M1<-solve(M) # ma trận M1 là ma trận nghịch đảo của ma trận M
M1 %*% M # tích của M1 với M là ma trận đơn vị## [,1] [,2] [,3] [,4]
## [1,] 1.000000e+00 -1.387779e-16 -2.220446e-16 -1.040834e-16
## [2,] -2.081668e-17 1.000000e+00 0.000000e+00 1.387779e-17
## [3,] -6.938894e-18 1.110223e-16 1.000000e+00 -2.775558e-17
## [4,] -3.469447e-17 1.665335e-16 1.110223e-16 1.000000e+00
M %*% M1 # tích của M với M1 là ma trận đơn vị## [,1] [,2] [,3] [,4]
## [1,] 1.000000e+00 -1.734723e-17 -3.053113e-16 3.122502e-17
## [2,] -5.551115e-17 1.000000e+00 5.551115e-17 -1.144917e-16
## [3,] 0.000000e+00 0.000000e+00 1.000000e+00 5.551115e-17
## [4,] 0.000000e+00 1.873501e-16 2.775558e-17 1.000000e+00
Các tính toán liên quan đến định thức cần nhớ
Định thức của ma trận sau khi nhân tất cả các phần tử với một số \[\begin{align} det(\lambda M) = \lambda^n \times det(M) \end{align}\]
Định thức của ma trận chuyển vị \(M^{-T}\) bằng định thức của ma trận \(M\) \[\begin{align} det(M^T) = det(M) \end{align}\]
Tích của định thức của ma trận \(M^{-1}\) với định thức của ma trận \(M\) bằng 1. \[\begin{align} det(M^{-1}) \times det(M) = det(I_n) = 1 \end{align}\]
2.2 Mảng nhiều chiều
Ma trận lưu phần tử trong hai chiều mà chúng ta gọi là hàng và cột. Đa số dữ liệu kiểu bảng biểu truyền thống đều có thể biểu diễn dưới dạng ma trận. Tuy nhiên có những kiểu dữ liệu mà khi biểu diễn dưới dạng ma trận hai chiều là không dễ dàng và có thể gây nhầm lẫn cho người sử dụng. Có thể kể đến dữ liệu kiểu hình ảnh. Khi bạn đọc lưu một bức ảnh màu lên trên máy tính điện tử, bức ảnh sẽ được số hóa thành một mảng ba chiều, bao gồm có chiều cao, chiều rộng của ảnh và một chiều thứ ba là màu sắc của điểm ảnh. Phức tạp hơn nữa nếu dữ liệu là một đoạn phim, hay một hình động, bạn đọc sẽ cần phải sử dụng thêm chiều thứ tư để mô tả thời gian xuất hiện của mỗi hình ảnh trong đoạn phim.
Thông thường thì người làm việc với dữ liệu sẽ đổi các mảng nhiều chiều về mảng hai chiều hoặc một chiều (véc-tơ) để dễ dàng xử lý. Các thao tác cơ bản khi làm việc với mảng nhiều chiều là cần thiết để thực hiện xử lý dữ liệu một cách chính xác.
2.2.1 Khởi tạo mảng nhiều chiều
Để tạo mảng nhiều chiều bạn đọc sử dụng hàm \(array()\).
## , , 1
##
## [,1] [,2] [,3]
## [1,] 1 3 5
## [2,] 2 4 6
##
## , , 2
##
## [,1] [,2] [,3]
## [1,] 7 9 11
## [2,] 8 10 12
##
## , , 3
##
## [,1] [,2] [,3]
## [1,] 13 15 17
## [2,] 14 16 18
##
## , , 4
##
## [,1] [,2] [,3]
## [1,] 19 21 23
## [2,] 20 22 24
Bạn đọc có thể thấy R hiển thị mảng ba chiều \(Ar\) kích thước \(2 \times 3 \times 4\) như là sự kết hợp của 4 ma trận kích thước \(2 \times 3\). Tương tự như khi khởi tạo giá trị cho ma trận, số lượng phần tử đưa vào trong mảng phải bằng với số phần tử của mảng, trong trường hợp mảng \(Ar\) ở trên là véc-tơ có độ dài 24 tương ứng với \(2 \times 3 \times 4 = 24\) phần tử của mảng.
Để lấy ra các phần tử con (một biến, một ma trận, hay 1 mảng nhiều chiều) từ một nhiều mảng, bạn đọc sử dụng \([]\) giống như khi làm với ma trận. Lưu ý rằng khi lấy phần tử con từ một mảng, bạn đọc cần phải sử dụng chỉ số cho tất cả các chiều.
Ar[1,2,1] # phần tử có các chỉ số 1 - 2 - 1 ## [1] 3
Ar[,,1] # ma trận 2 * 3## [,1] [,2] [,3]
## [1,] 1 3 5
## [2,] 2 4 6
## , , 1
##
## [,1] [,2]
## [1,] 1 5
## [2,] 2 6
##
## , , 2
##
## [,1] [,2]
## [1,] 19 23
## [2,] 20 24
Thứ tự các phần tử khi điền vào mảng khi sử dụng hàm trong hàm array() sẽ là ưu tiên ma trận kích thước \(2 \times 3\) tương ứng với chỉ số \([,,1]\) trước, rồi đến ma trận kích thước \(2 \times 3\) tương ứng với chỉ số \([,,2]\),…, và tiếp tục như thế cho đến khi tất cả các phần tử của mảng được gán giá trị.
2.2.2 Sử dụng mảng nhiều chiều để biến đổi dữ liệu kiểu hình ảnh
Để bạn đọc có cái nhìn trực quan hơn về mảng nhiều chiều, chúng ta sẽ thực hiện các phép biến đổi, tính toán trên một dữ liệu cụ thể. Như chúng tôi đã nói ở trên, mảng nhiều chiều là một đối tượng thích hợp dùng để lưu dữ liệu kiểu hình ảnh. Thư viện \(imager\) được cài thêm trên R có các hàm thích hợp để làm việc với dữ liệu kiểu hình ảnh. Chúng ta sẽ sử dụng một mảng nhiều chiều để lưu một bức ảnh và thực hiện các phép biến đổi bức ảnh đó sử dụng tính toán trên mảng nhiều chiều.
Hàm load.image() trong thư viện \(imager\) có thể đọc các file hình ảnh có định dạng \(png\), \(jpeg\), hoặc \(bmp\). Bạn đọc có thể đọc một hình ảnh có một trong các định dạng kể trên
setwd("../KHDL_KTKD/Image")
img<-load.image("cat.jpg") # đọc hình ảnh tên "cat" vào
plot(img)
Để biết \(img\) là kiểu đối tượng nào, bạn đọc dùng hàm class()
class(img)## [1] "cimg" "imager_array" "numeric"
R cho biết đây là một đối tượng kiểu \(cimg\). Kiểu đối tượng này về bản chất là một mảng bốn chiều. Chiều thứ nhất là cho biết chiều rộng của bức ảnh, chiều thứ hai cho biết chiều cao của bức ảnh, chiều thứ ba luôn bằng 1 đối với dữ liệu kiểu ảnh và chiều thứ tư bằng 3 nếu bức ảnh là ảnh màu.
Đối tượng kiểu \(cimg\) cho phép bạn đọc thực hiện các biến đổi, tính toán giống như trên một mảng nhiều chiều mà không cần phải chuyển đổi sang kiểu mảng. Chẳng hạn như để biết bức ảnh được lưu bởi đối tượng tên \(img\) có bao nhiêu chiều, chúng ta sử dụng hàm dim() giống như với mảng
dim(img) # mảng bốn chiều: chiều rộng * chiều cao * chiều thời gian * chiều màu sắc## [1] 535 595 1 3
Bức ảnh được lưu bởi đối tượng \(img\) ở trên là một bức ảnh màu có chiều rộng 535 và chiều cao 595. Chiều thứ ba bằng 1 nghĩa là đây là một bức ảnh (chiều thứ ba lớn hơn 1 khi đối tượng là hình ảnh động). Chiều thứ tư bằng 3 đại diện cho 3 màu đỏ (Red), màu xanh lá cây (Green), và màu xanh da trời (Blue). Bạn đọc có thể hình dung một bức ảnh màu ở trên như là sự kết hợp của ba ma trận cùng kích thước \(535 \times 595\), ma trận thứ nhất đại diện cho màu đỏ, ma trận thứ hai đại diện cho màu xanh lá cây và ma trận thứ ba đại diện cho màu xanh da trời. Mỗi giá trị trong ma trận là một số trong khoảng từ 0 đến 1. Giá trị 0 tương ứng với màu đen và giá trị càng gần 1 thì màu sắc của điểm đó càng gần màu mà ma trận đại diện. Để quan sát ma trận tương ứng với mỗi màu, bạn đọc cần gán giá trị của 2 ma trận còn lại bằng 0 trước khi hiển thị.
img_red<-img
img_red[,,1,2:3]<-0 # cho 2 ma trận màu xanh lá cây và xanh da trời bằng 0
img_green<-img
img_green[,,1,c(1,3)]<-0 # cho 2 ma trận màu đỏ và xanh da trời bằng 0
img_blue<-img
img_blue[,,1,1:2]<-0 # cho 2 ma trận màu xanh lá cây và đỏ bằng 0
par(mfrow = c(1,3))
plot(img_red, main = "Chỉ giữa lại màu đỏ")
plot(img_green, main = "Chỉ giữa lại màu xanh lá cây")
plot(img_blue, main = "Chỉ giữa lại màu xanh da trời")
Hàm as.cimg() được dùng để đổi một mảng bốn chiều sang kiểu \(cimg\) để có thể hiển thị khi sử dụng hàm plot(). Lưu ý rằng hãy luôn sử dụng chiều thứ ba bằng 1 và chiều thứ tư bằng 3 nếu bạn muốn tạo ảnh màu. Các câu lệnh dưới đây tạo ra các bức ảnh mà các giá trị trong các ma trận màu sắc hoàn toàn là các giá trị ngẫu nhiên phân phối đều (uniform) trong khoảng (0,1).
img1<-array(runif(5*5*1*3),dim=c(5,5,1,3)) # bức ảnh màu kích thước 5*5
img1<-as.cimg(img1)
img2<-array(runif(1000*1000*1*3),dim=c(1000,1000,1,3)) # bức ảnh màu kích thước 1000*1000
img2<-as.cimg(img2)
par(mfrow = c(1,2))
plot(img1, interpolate = FALSE, main = "Ảnh nhiễu kích thước 5 * 5")
plot(img2, interpolate = FALSE, main = "Ảnh nhiễu kích thước 1000 * 1000")
Tham số \(interpolate\) nhận giá trị bằng \(FALSE\) có nghĩa là các điểm ảnh giữ nguyên giá trị. Tham số này có giá trị là mặc định là \(TRUE\). Khi \(interpolate\) nhận giá trị bằng \(TRUE\) hình ảnh hiển thị sẽ có sự giao thoa về màu sắc tại viền các điểm ảnh và làm cho ảnh nhìn mượt mà hơn.
Về bản chất, xử lý ảnh trên máy tính điện tử chính là xử lý các con số nằm trong mảng nhiều chiều. Chúng tôi sẽ giới thiệu một vài kỹ thuật xử lý đơn giản trên ảnh để bạn đọc có thể hiểu hơn về xử lý mảng nhiều chiều. Trước hết là thao tác cắt ảnh. Cắt ảnh chính là một phép lấy mảng con từ một mảng ban đầu. Thật vậy,
n<-dim(img)[1] # chiều rộng của ảnh
k<-round(n/2) # điểm giữa để chia ảnh làm hai nửa
img1<-img[1:k,,,] # img1 là nửa bên trái của ảnh
img2<-img[(k+1):n,,,] # img2 là nửa bên phải của ảnh
par(mfrow = c(1,2))
plot(as.cimg(img1), main = "Nửa bên trái")
plot(as.cimg(img2), main = "Nửa bên phải")
Tiếp theo, chúng ta sẽ thực hiện tăng hoặc giảm độ sáng của ảnh. Tăng hoặc giảm độ sáng của ảnh tương đương với việc điều chỉnh đồng thời các số trong mảng nhiều chiều gần hơn đến giá trị 1 hoặc gần hơn đến giá trị 0. Chúng ta thực hiện như sau
img1<-img + (1 - img) * 0.3 # img1 là bức ảnh sau khi tăng độ sáng lên 30%
img2<-img - img * 0.3 # img2 là bức ảnh sau khi giảm độ sáng đi 30%
par(mfrow = c(1,3))
plot(img, rescale = FALSE, main= "Ảnh ban đầu")
plot(as.cimg(img1), rescale = FALSE, main = "Tăng độ sáng")
plot(as.cimg(img2),rescale = FALSE, main = "Giảm độ sáng")
Một kỹ thuật xử lý ảnh khác là giảm kích thước của ảnh. Giả sử bạn đọc muốn giảm kích thước ảnh mỗi chiều 50%. Để làm được việc này, mỗi ma trận kích thước \(n \times m\) sẽ được đổi thành ma trận kích thước \([n/2] \times [m/2]\) trong đó \([n/2]\) là phần nguyên của số \(n/2\). Nguyên tắc chuyển từ ma trận ban đầu sang ma trận có kích thước nhỏ hơn là mỗi ô \(2 \times 2\) của ma trận ban đầu được chuyển thành 1 số trong ma trận mới
giamchieu<-function(M,k){
n<-dim(M)[1]; m<-dim(M)[2]
n1<-round(n/k)-1; m1<-round(m/k)-1
M1<-matrix(0,n1,m1)
for (i in 1:n1){
for (j in 1:m1){
i1<-(k*(i-1)+1):(k*i)
j1<-(k*(j-1)+1):(k*j)
M1[i,j]<-mean(M[i1,j1],na.rm=TRUE)
}
}
return(M1)
}
n<-dim(img)[1]; m<-dim(img)[2]
k1<-10; k2<-20
n1<-round(n/k1)-1; m1<-round(m/k1)-1
n2<-round(n/k2)-1; m2<-round(m/k2)-1
img1<-array(0,dim=c(n1,m1,1,3));
img2<-array(0,dim=c(n2,m2,1,3));
for (i in 1:3){
img1[,,1,i]<-giamchieu(img[,,1,i],k1)
img2[,,1,i]<-giamchieu(img[,,1,i],k2)
}
par(mfrow = c(1,3))
plot(img, interpolate = FALSE, main= "Ảnh ban đầu")
plot(as.cimg(img1), interpolate = FALSE, main = "Giảm kích thước 1:10")
plot(as.cimg(img2), interpolate = FALSE, main = "Giảm kích thước 1:20")
2.3 List
Không giống như véc-tơ, ma trận, hay mảng nhiều chiều, \(list\) là một cấu trúc trong R mà có thể chứa nhiều kiểu đối tượng khác nhau bao gồm biến, véc-tơ, ma trận, và cả các \(list\) khác. Với những bạn đọc đã học qua Python, \(list\) cũng giống như một \(dictionary\). Đối với các bạn đọc đã học qua ngôn ngữ lập trình C++, \(list\) tương tự như một \(struct\). \(list\) đóng vai trò quan trọng trong R, đặc biệt là trong viết hàm số và lập trình hướng đối tượng.
Trong phần này của cuốn sách, chúng ta sẽ tìm hiểu cách tạo ra \(list\) và ứng dụng cấu trúc của \(list\) để phục vụ công việc phân tích dữ liệu một cách hiệu quả nhất.
2.3.1 Khởi tạo \(list\) và chỉ số của \(list\).
Hàm số để tạo ra một \(list\) trong R là hàm list(). Giả sử chúng ta muốn tạo thành một đối tượng có tên \(SV1\) chứa các thông tin về một sinh viên
Tên của sinh viên: được lưu trong một biến kiểu chuỗi ký tự.
Ngày sinh của sinh viên: được lưu trong một biến kiểu thời gian.
Giới tính của sinh viên: được lưu trong một biến kiểu logic, giá trị \(TRUE\) tương ứng với giới tính Nam, và \(FALSE\) tương ứng với giới tính nữ.
Bảng điểm của sinh viên: là một \(data.frame\) có 2 cột, 1 cột là tên môn học và một cột là điểm của môn học tương ứng.
Chúng ta sử dụng hàm list() để tạo ra một \(list\) có tên \(SV1\) như sau
SV1<-list(Ten = "Nguyễn Đức Nam",
Ngay_sinh = as.Date("2000-06-20"),
Gioi_tinh = TRUE,
Bang_diem = data.frame(Mon_hoc = c("Giải tích", "Đại số", "Xác suất"),
Diem = c(6.5, 8.5, 7.0)))
str(SV1) # xem cấu trúc của list SV1## List of 4
## $ Ten : chr "Nguyễn Đức Nam"
## $ Ngay_sinh: Date[1:1], format: "2000-06-20"
## $ Gioi_tinh: logi TRUE
## $ Bang_diem:'data.frame': 3 obs. of 2 variables:
## ..$ Mon_hoc: chr [1:3] "Giải tích" "Đại số" "Xác suất"
## ..$ Diem : num [1:3] 6.5 8.5 7
Bạn đọc có thể thấy \(SV1\) có bốn đối tượng con có tên là \(Ten\), \(Ngay\_sinh\), \(Gioi\_tinh\), và \(Bang\_diem\). Mỗi đối tượng con có một kiểu giá trị khác nhau, riêng đối tượng con \(Bang\_diem\) là một dữ liệu (\(data.frame\)).
Để lấy ra một đối tượng con của \(list\) bạn đọc sử dụng ký hiệu \(\$\). Chẳng hạn bạn đọc muốn hiển thị bảng điểm của sinh viên có thông tin được lưu trong \(list\) SV1, bạn đọc sử dụng câu lệnh sau
SV1$Bang_diem # hiển thị bảng điểm## Mon_hoc Diem
## 1 Giải tích 6.5
## 2 Đại số 8.5
## 3 Xác suất 7.0
Để biết tên các đối tượng trong một \(list\), bạn đọc sử dụng hàm names()
names(SV1)## [1] "Ten" "Ngay_sinh" "Gioi_tinh" "Bang_diem"
Một cách khác để lấy ra một đối tượng con của \(list\) là sử dụng chỉ số của đối tượng. Do bảng điểm nằm ở vị trí thứ 4 trong list nên bạn đọc sử dụng câu lệnh sau
SV1[[4]] # sử dụng 2 lần dấu ngoặc vuông## Mon_hoc Diem
## 1 Giải tích 6.5
## 2 Đại số 8.5
## 3 Xác suất 7.0
Bạn đọc có thể thấy rằng để lấy ra phần tử con, chúng ta cần phải sử dụng hai lần dấu ngoặc vuông. Nếu chỉ sử dụng một dấu ngoặc vuông, phần tử được lấy ra sẽ là 1 list có 1 phần tử và phần tử duy nhất là bảng điểm.
SV1[4] # là một list có 1 phần tử## $Bang_diem
## Mon_hoc Diem
## 1 Giải tích 6.5
## 2 Đại số 8.5
## 3 Xác suất 7.0
Để thêm một đối tượng vào \(list\), chúng ta có thể đặt tên trực tiếp cho đối tượng mới và gán giá trị cho đối tượng. Ví dụ như chúng ta muốn thêm thông tin về quê quán của sinh viên vào một đối tượng có tên là \(que\_quan\)
SV1$que_quan<-"Hà Nội" # Thêm vào một phần tử có tên que_quan là một biến
str(SV1) # list SV1 đã có thêm phần tử thứ năm## List of 5
## $ Ten : chr "Nguyễn Đức Nam"
## $ Ngay_sinh: Date[1:1], format: "2000-06-20"
## $ Gioi_tinh: logi TRUE
## $ Bang_diem:'data.frame': 3 obs. of 2 variables:
## ..$ Mon_hoc: chr [1:3] "Giải tích" "Đại số" "Xác suất"
## ..$ Diem : num [1:3] 6.5 8.5 7
## $ que_quan : chr "Hà Nội"
Để xóa đi một đối tượng khỏi list, chúng ta gán cho đối tượng đó giá trị bằng \(NULL\)
SV1$que_quan<-NULL # xóa phần tử có tên que_quan khỏi SV1
str(SV1) # list SV1 chỉ còn 4 phần tử## List of 4
## $ Ten : chr "Nguyễn Đức Nam"
## $ Ngay_sinh: Date[1:1], format: "2000-06-20"
## $ Gioi_tinh: logi TRUE
## $ Bang_diem:'data.frame': 3 obs. of 2 variables:
## ..$ Mon_hoc: chr [1:3] "Giải tích" "Đại số" "Xác suất"
## ..$ Diem : num [1:3] 6.5 8.5 7
Như chúng ta đã thảo luận trong phần giới thiệu, \(list\) là một cấu trúc nhiều lớp, nghĩa là một list có thể chứa các đối tượng có kiểu \(list\). Thật vậy, giả sử chúng ta có list \(SV2\) chứa các thông tin tương ứng của một sinh viên khác
SV2<-list(Ten = "Nguyễn Thị Loan",
Ngay_sinh = as.Date("2000-05-13"),
Gioi_tinh = FALSE,
Bang_diem = data.frame(Mon_hoc = c("Xác suất", "Thống kê", "Học máy","AI"),
Diem = c(7.0, 9.5, 10.0, 9.0)),
Que_quan = "Hà Nội")Chúng ta có thể tạo một \(list\) có tên là \(DS\) chứa thông tin của cả 2 sinh viên
DS<-list(SV1 = SV1,SV2 = SV2) # DS là một list có 2 phần tử, mỗi phần tử là 1 list
str(DS) # xem cấu trúc của list DS## List of 2
## $ SV1:List of 4
## ..$ Ten : chr "Nguyễn Đức Nam"
## ..$ Ngay_sinh: Date[1:1], format: "2000-06-20"
## ..$ Gioi_tinh: logi TRUE
## ..$ Bang_diem:'data.frame': 3 obs. of 2 variables:
## .. ..$ Mon_hoc: chr [1:3] "Giải tích" "Đại số" "Xác suất"
## .. ..$ Diem : num [1:3] 6.5 8.5 7
## $ SV2:List of 5
## ..$ Ten : chr "Nguyễn Thị Loan"
## ..$ Ngay_sinh: Date[1:1], format: "2000-05-13"
## ..$ Gioi_tinh: logi FALSE
## ..$ Bang_diem:'data.frame': 4 obs. of 2 variables:
## .. ..$ Mon_hoc: chr [1:4] "Xác suất" "Thống kê" "Học máy" "AI"
## .. ..$ Diem : num [1:4] 7 9.5 10 9
## ..$ Que_quan : chr "Hà Nội"
Để xem bảng điểm của sinh viên thứ hai, chúng ta cần sử dụng 2 lần ký hiệu \(\$\):
DS$SV2$Bang_diem # Xem bảng điểm của sinh viên thứ hai## Mon_hoc Diem
## 1 Xác suất 7.0
## 2 Thống kê 9.5
## 3 Học máy 10.0
## 4 AI 9.0
2.3.2 Sử dụng \(list\) với hàm số
Hầu như tất cả các hàm số được xây dựng sẵn trong R đều cho kết quả đầu ra dưới dạng \(list\). Bạn đọc quan sát giá trị đầu ra của hàm có tên là uniroot() như sau
## [1] "list"
str(result) # xem cấu trúc của đối tượng result## List of 5
## $ root : num 0.5
## $ f.root : num -2.85e-05
## $ iter : int 6
## $ init.it : int NA
## $ estim.prec: num 6.1e-05
Hàm uniroot() được sử dụng để tìm nghiệm duy nhất của một hàm số trên một khoảng. Đoạn câu lệnh ở trên sử dụng hàm \(uniroot()\) để tìm nghiệm duy nhất của phương trình \(x^2 - 1/4 = 0\) trên khoảng \((0,1)\). Kết quả của hàm uniroot() là một list có 5 phần tử đều là các biến kiểu số với tên tương ứng là \(root\), \(f.root\), \(iter\), \(init.it\), và $ estim.prec$. Các hàm số phức tạp hơn sẽ có cấu trúc của kết quả đầu ra phức tạp hơn rất nhiều. Bạn đọc cần đọc kỹ hướng dẫn của các hàm để hiểu mỗi đối tượng con của kết quả đầu ra có ý nghĩa như thế nào.
Bạn đọc cũng nên sử dụng kiểu \(list\) để làm đầu ra cho các hàm số tự xây dựng. Chúng ta sẽ quay trở lại ví dụ về xây dựng hàm số \(PV\) để tính giá trị hiện tại của một dòng tiền. Đối với một dòng tiền tương lai, ngoài giá trị hiện tại, bạn đọc có thể quan tâm đến các giá trị khác như Macaulay Duration, Modified Duration, Dollar Duration, Convexity (ý nghĩa và cách tính các giá trị này ở trong phần phụ lục của chương này).
summaryCF<-function(i,CF){
n<-length(CF)
PV<-sum(CF/((1+i)^(1:n)))
Mac_D<-sum(CF*(1:n)/((1+i)^(1:n)))/PV
Mod_D<-Mac_D/(1+i)
Dollar_D<-PV*Mod_D*100
Convexity<-Mod_D
ket_qua<-list(PV = PV, Mac_D = Mac_D, Mod_D = Mod_D,
Dollar_D = Dollar_D, Convexity = Convexity)
return(ket_qua)
}Chúng ta có thể sử dụng hàm summary_CF() để tính toán các đặc trưng của một trái phiếu với các thông số như sau:
| Thông số trái phiếu | Giá trị |
|---|---|
| Ngày hiện tại | Ngày 01 tháng 10 năm 2023 |
| Ngày đáo hạn | Ngày 30 tháng 09 năm 2035 |
| Mệnh giá | 10 tỷ Vnd |
| Lãi suất Coupon | 9,25% |
| Số lần trả coupon trong năm | 1 lần/năm |
| Lãi suất chiết khấu | 5,00% |
Chúng ta tạo ra dòng tiền tương lai của trái phiếu với các thông số như trên và sau đó sử dụng hàm summary_CF()
# Nhập liệu
F<-10 # mệnh giá trái phiếu, đơn vị tỷ đồng
T<-12 # 12 cho đến ngày đáo hạn
c<-9.25/100 # lãi suất coupon
i<-5/100 # lãi suất dùng để chiết khấu
CF<-c(rep(c*F,(T-2)),c*F+F) # Dòng tiền tương lai của trái phiếu
summaryCF(i,CF)## $PV
## [1] 13.53023
##
## $Mac_D
## [1] 7.884908
##
## $Mod_D
## [1] 7.509436
##
## $Dollar_D
## [1] 10160.44
##
## $Convexity
## [1] 7.509436
Chuyển sang một ví dụ khác khi sử dụng \(list\) làm đầu ra cho một hàm số tự xây dựng. Khi bạn đọc tìm hiểu về giá trị của một véc-tơ kiểu số, chúng ta thường tính toán các giá trị đặc trưng như giá trị trung bình, giá trị lớn nhất, nhỏ nhất, các phân vị, và muốn xem phân phối các giá trị trong véc-tơ đó như thế nào. Chúng ta có thể tự viết một hàm số để thực hiện việc này với đầu ra là một \(list\)
summary_vec<-function(x){
do_dai<-length(x) # độ dài của véc-tơ
ty_le_na<-paste(round(sum(is.na(x))/do_dai*100,2),"%") # % giá trị không quan sát được
gioi_han<-c(min(x,na.rm=TRUE),max(x,na.rm=TRUE)) # lớn nhất và nhỏ nhất
trung_binh<-mean(x,na.rm=TRUE)
do_lech_chuan<-sd(x,na.rm=TRUE)
phan_vi<-quantile(x,c(0.01,0.1,0.25,0.5,0.75,0.9,0.99),na.rm=TRUE)
do_thi<-ggplot(data=data.frame(x=x), aes(x=x))+
geom_histogram(color = "black", fill = "blue",alpha = 0.5)+
xlab("")+ylab("")
result<-list(do_dai = do_dai, ty_le_na = ty_le_na, gioi_han = gioi_han,
trung_binh = trung_binh, do_lech_chuan = do_lech_chuan,
phan_vi = phan_vi, do_thi = do_thi)
return(result)
}Chúng ta có thể sử dụng hàm summary_vec() để tổng hợp thông tin về lợi suất tính theo ngày của chỉ số FTSE (chỉ số cổ phiếu của 100 công ty có giá trị vốn hóa thị trường lớn nhất niêm yết trên Sở giao dịch chứng khoán London) trong năm 1991 đến năm 1999. Chỉ số này được lưu trong dữ liệu \(EuStockMarkets\) có sẵn trong R.
chi_so<-EuStockMarkets[,4] # lấy chỉ số FTSE ra từ cột thứ 4 của EuStockMarkets
n<-length(chi_so) # độ dài của chuỗi chỉ số chứng khoán
loi_suat<-log(chi_so[2:n]/chi_so[1:(n-1)]) # lợi suất của chỉ số
summary_vec(loi_suat)## $do_dai
## [1] 1859
##
## $ty_le_na
## [1] "0 %"
##
## $gioi_han
## [1] -0.04139903 0.05439552
##
## $trung_binh
## [1] 0.0004319851
##
## $do_lech_chuan
## [1] 0.007957728
##
## $phan_vi
## 1% 10% 25% 50% 75%
## -2.060655e-02 -9.139666e-03 -4.318778e-03 8.021069e-05 5.253592e-03
## 90% 99%
## 9.714781e-03 1.931723e-02
##
## $do_thi

Một lợi thế khác của đối tượng kiểu \(list\) là đẩy nhanh tốc độ tính toán khi dùng các hàm họ apply(). Chúng ta sẽ thảo luận vấn đề này trong phần tiếp theo của cuốn sách.
2.4 Các hàm họ apply()
Nhóm hàm apply() là nhóm hàm có sẵn trong R cho phép bạn đọc thực hiện lặp đi lặp lại một hàm số trên nhiều đối tượng. Về cơ bản nhóm hàm này hoạt động giống như một vòng lặp nhưng câu lệnh viết bằng nhóm hàm này sẽ chạy nhanh hơn và đơn giản hơn viết vòng lặp rất nhiểu.
Các hàm mà chúng tôi sẽ giới thiệu đến bạn đọc trong phần này bao gồm apply(), lapply() và sapply. Còn nhiều hàm khác thuộc nhóm hàm này như vapply(), tapply(), mapply(), …, nhưng về nguyên tắc hoạt động của các hàm này là tương tự và chỉ khác ở chỗ chúng áp dụng trên các loại đối tượng khác nhau nên bạn đọc có thể tự tìm hiểu mà không gặp khó khăn nào.
2.4.1 Hàm apply()
Cho một véc-tơ \(x\) kiểu số và một hàm \(f\), chẳng hạn như \(f(x) = x^2\). Khi bạn đọc viết f(x) R sẽ hiểu rằng bạn đang thực hiện hàm số \(f\) cho từng phần tử của véc-tơ \(x\) và sẽ trả lại giá trị là một véc-tơ mà từng phần tử tương ứng là bình phương của các phần tử trong \(x\). Việc thực hiện hàm \(f\) trên véc-tơ \(x\) diễn ra một cách đồng thời và hiệu quả hơn so với việc viết một vòng lặp để tính hàm \(f\) trên từng phần tử của \(x\).
x<-1:5; f<-function(x) x^2
f(x) # f được áp dụng trên từng phần tử của x## [1] 1 4 9 16 25
Điều gì xảy ra khi \(x\) không phải là một véc-tơ đồng các phần tử con của \(x\) không phải là một biến, chẳng hạn như
\(x\) là một ma trận và bạn muốn tính toán một hàm \(f\) trên các phần tử con của \(x\) là một véc-tơ hàng hoặc một véc-tơ cột.
\(x\) là một dữ liệu và bạn muốn thực hiện một hàm \(f\) trên tất cả các cột dữ liệu.
\(x\) là một list và bạn muốn thực hiện một hàm \(f\) trên tất cả các đối tượng con của \(x\).
Các hàm thuộc họ apply() sẽ giúp bạn đọc thực hiện tác tính toán như vậy. Cách viết hàm apply() như sau:
apply(x, MARGIN, FUN, ...)trong đó \(x\) là một ma trận, một mảng nhiều chiều, hoặc một dữ liệu; \(MARGIN\) là một số, hoặc véc-tơ chỉ số cho biết hàm sẽ áp dụng trên chiều (hoặc các chiều) nào, và \(FUN\) là hàm số mà bạn muốn thực hiện. Ví dụ như bạn đọc muốn tính giá trị trung bình của mỗi cột của một ma trận \(M\), hãy sử dụng câu lệnh như sau
M<-matrix(1:100,20,5) # ma trận kích thước 20 * 5
apply(M, MARGIN = 2, FUN = mean) # MARGIN = 2 nghĩa là áp dụng theo cột (1 nghĩa là theo hàng) ## [1] 10.5 30.5 50.5 70.5 90.5
Do ma trận \(M\) có 5 cột nên giá trị trả lại là một véc-tơ kiểu số có độ dài bằng 5. Véc-tơ này chứa giá trị là trung bình của các cột thứ tự 1, 2, 3, 4, và 5 của ma trận \(M\).
Về nguyên tắc đối tượng sử dụng trong hàm apply() là ma trận hoặc mảng nhiều chiều. Bạn đọc cũng có thể sử dụng hàm apply() trên đối tượng là dữ liệu (data.frame). Khi đối tượng của hàm apply() có từ 3 chiều trở lên, giá trị của tùy biến \(MARGIN\) có thể là một số hoặc một véc-tơ. Thật vậy,
Ar<-array(1:20,dim=c(5,2,2)) # mảng kích thước 5 * 2 * 2
apply(Ar, MARGIN = 3, FUN = mean) # MARGIN = 3 nghĩa là áp dụng hàm mean theo chiều thứ 3 ## [1] 5.5 15.5
Giá trị trả lại sẽ là một véc-tơ có độ dài là 2, phần tử thứ nhất là giá trị trung bình của các phần tử thuộc ma trận kích thước \(5 \times 2\) thứ nhất (ma trận \(Ar[,,1]\)) và phần tử thứ hai là giá trị trung bình của các phần tử thuộc ma trận kích thước \(5 \times 2\) thứ hai (ma trận \(Ar[,,2]\)). Chúng ta có thể kiểm tra kết quả như sau:
mean(Ar[,,1]) # bằng phần tử thứ nhất khi dùng apply## [1] 5.5
mean(Ar[,,2]) # bằng phần tử thứ hai khi dùng apply## [1] 15.5
Chúng ta có thể áp dụng đồng thời hàm mean() theo chiều thứ 2 và chiều thứ 3 trên mảng \(Ar\) như sau
## [,1] [,2]
## [1,] 3 13
## [2,] 8 18
Kết quả thu được sẽ là một ma trận kích thước \(2 \times 2\) mà các phần tử sẽ tương ứng với giá trị trung bình:
Phần tử ở vị trí [1,1] của ma trận kết quả là giá trị trung bình của véc-tơ \(Ar[,1,1]\)
Phần tử ở vị trí [1,2] của ma trận kết quả là giá trị trung bình của véc-tơ \(Ar[,1,2]\)
Phần tử ở vị trí [2,1] của ma trận kết quả là giá trị trung bình của véc-tơ \(Ar[,2,1]\)
Phần tử ở vị trí [2,2] của ma trận kết quả là giá trị trung bình của véc-tơ \(Ar[,2,2]\)
Chúng ta có thể so sánh giá trị trung bình của các véc-tơ với ma trận kết quả của hàm apply():
mean(Ar[,1,1]) # bằng phần tử ở vị trí [1,1] của ma trận kết quả## [1] 3
mean(Ar[,1,2]) # bằng phần tử ở vị trí [1,2] của ma trận kết quả## [1] 13
mean(Ar[,2,1]) # bằng phần tử ở vị trí [1,2] của ma trận kết quả## [1] 8
mean(Ar[,2,2]) # bằng phần tử ở vị trí [1,2] của ma trận kết quả## [1] 18
Hàm số sử dụng với tùy biến \(FUN\) có thể là hàm số có sẵn trong R và các thư viện cài đặt bổ sung, hoặc cũng có thể là một hàm số mà bạn đọc tự xây dựng. Khi các câu lệnh của hàm số tự xây dựng ngắn gọn, bạn đọc có thể định nghĩa hàm số đó bên trong hàm apply(). Giá trị đầu ra của hàm số được tự định nghĩa cũng có thể là một véc-tơ, thậm chí là một ma trận hay là một mảng nhiều chiều, thậm chí là một \(list\). Nếu kết quả đầu ra của hàm sử dụng trong apply() là một ma trận hoặc mảng nhiều chiều, R sẽ chuyển ma trận hoặc mảng nhiều chiều về dạng véc-tơ. Trong trường hợp đầu ra của hàm tự định nghĩa là một \(list\), giá trị đầu ra sẽ là một ma trận hoặc mảng nhiều chiều mà mỗi phần tử con là một list. Dưới đây là một ví dụ mà giá trị đầu ra là một véc-tơ ba chiều.
## [,1] [,2] [,3] [,4] [,5]
## [1,] 1.0 21.0 41.0 61.0 81.0
## [2,] 10.5 30.5 50.5 70.5 90.5
## [3,] 20.0 40.0 60.0 80.0 100.0
Kết quả nhận được sẽ là một ma trận kích thước \(3 \times 5\). Cột thứ nhất của ma trận kết quả nhận giá trị (1, 10.5, 20) tương ứng với các giá trị \(min\), \(mean\), và \(max\) của véc-tơ \(M[,1]\).
M[,1]## [1] 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
## [1] 1.0 10.5 20.0
Bạn đọc cũng có thể tự xây dựng hàm số trên môi trường chung sau đó gọi tên hàm số này trong hàm apply(). Nếu hàm số tự xây dựng là hàm số có tham số khác ngoài \(x\), bạn đọc cần phải khai báo giá trị cho tham số đó trong môi trường cục bộ của hàm apply():
my_range<-function(x, a) (max(x^a) - min(x^a)) # hàm số có tham số khác là a
apply(M, 2, my_range, a = 2) # cần khai báo giá trị tham số a của my_range trong hàm apply## [1] 399 1159 1919 2679 3439
Khi tham số của hàm số mà bạn đọc muốn áp dụng trên ma trận là không cố định mà thay đổi theo một chiều của \(x\) thì không nên khai báo giá trị của tham số theo dạng véc-tơ trong hàm apply(). Thật vậy, giả sử bạn đọc muốn tính các giá trị phân vị ở mức xác suất 10% và 90% lần lượt của véc-tơ hàng thứ nhất và véc-tơ hàng thứ hai của một ma trận M kích thước \(2 \times 10\). Hàm số quantile(x, probs = p) là hàm số có sẵn trong R được sử dụng để tính giá trị phân vị tại mức xác suất \(p\) của véc-tơ số \(x\). Hãy quan sát kết quả của hàm apply() khi sử dụng tham số \(probs\) của hàm quantile() dưới dạng véc-tơ:
M<-matrix(1:20,2,10) # ma trận 2 * 10
apply(M, 1, quantile, probs = c(0.1,0.9)) # MARGIN = 1 nghĩa là tính theo các hàng của M## [,1] [,2]
## 10% 2.8 3.8
## 90% 17.2 18.2
quantile(M[1,],0.1) # giá trị mong muốn## 10%
## 2.8
quantile(M[2,],0.9) # giá trị mong muốn## 90%
## 18.2
Bạn đọc có thể thấy rằng kết quả của hàm apply() khi tham số \(probs\) là một véc-tơ là một ma trận, trong đó cột thứ nhất là giá trị phân vị tại các mức xác suất 10% và 90% của véc-tơ \(M[1,]\) và cột thứ hai giá trị phân vị tại các mức xác suất 10% và 90% của véc-tơ \(M[2,]\). Giá trị chúng ta mong muốn lấy ra chính là các số nằm trên đường chéo chính của ma trận kết quả. Sẽ không có khó khăn gì nếu số lượng hàng của ma trận \(M\) nhỏ. Bạn đọc sẽ gặp vấn đề khi số lượng véc-tơ được áp dụng là lớn bởi kích thước của ma trận kết quả sẽ tăng lên theo cấp số nhân. Thật vậy, nếu \(M\) có \(n\) hàng và hàm số được áp dụng có 1 tham số, ma trận kết quả sẽ có kích thước sẽ là $ n n$ nếu bạn đọc sử dụng trực tiếp hàm apply(). Chẳng hạn như bạn muốn tính giá trị phân vị ở các mức xác suất 10%, 30%, 50%, 70%, và 90% của lần lượt các véc-tơ hàng thứ 1, 2, 3, 4, và 5 của một ma trận \(M\) kích thước \(5 \times 10\).
M<-matrix(1:50,5,10) # ma trận 5 * 10
apply(M, 1, quantile, probs = c(0.1,0.3,0.5,0.7,0.9)) # ma trận kết quả kích thước 5 * 5## [,1] [,2] [,3] [,4] [,5]
## 10% 5.5 6.5 7.5 8.5 9.5
## 30% 14.5 15.5 16.5 17.5 18.5
## 50% 23.5 24.5 25.5 26.5 27.5
## 70% 32.5 33.5 34.5 35.5 36.5
## 90% 41.5 42.5 43.5 44.5 45.5
## [1] 5.5 15.5 25.5 35.5 45.5
Điều gì xảy ra khi ma trận \(M\) có \(10^4\) véc-tơ hàng? Ma trận kết quả sẽ có kích thước là \(10^4 \times 10^4\). Khi ma trận \(M\) có \(10^5\) véc-tơ hàng? Ma trận kết quả sẽ có kích thước là \(10^5 \times 10^5\) và R sẽ báo lỗi vì bộ nhớ không đủ để lưu một ma trận có kích thước như vậy.
Một cách đơn giản để tiết kiệm thời gian và bộ nhớ khi áp dụng hàm số có tham số là hãy thêm tham số vào như là một phần của ma trận \(M\) và điều chỉnh lại hàm quantile() trước khi sử dụng hàm apply()
M1<-cbind(M,c(0.1,0.3,0.5,0.7,0.9)) # thêm tham số vào cột cuối của ma trận M
my_quantile<-function(x){ # định nghĩa lại hàm quantile mà tham số p là giá trị cuối cùng trong véc-tơ
n<-length(x)
quantile(x[1:(n-1)], x[n])
}
apply(M1, 1, my_quantile) # áp dụng hàm mới trên ma trận mới## [1] 5.5 15.5 25.5 35.5 45.5
2.4.2 Hàm lapply() và sapply().
Cơ chế hoạt động của lapply() tương tự như apply() và chỉ khác ở đối tượng áp dụng và cấu trúc của kết quả đầu ra. lapply() thường áp dụng trên các đối tượng kiểu \(list\) hoặc các kiểu đối tượng mà có thể sử dụng ký hiệu \(\$\) để gọi phần tử con chẳng hạn như \(data.frame\) hoặc \(tibbles\). Chúng ta sẽ thảo luận về các đối tượng này ở phần sau của cuốn sách. Khi bạn đọc sử dụng hàm lapply(), bạn không cần phải sử dụng tùy biến \(MARGIN\) bởi vì lapply() sẽ luôn luôn hiểu các đối tượng được tác động đến là tất cả các đối tượng con của \(list\).
x <- list(x1 = 1:10,
x2 = c(TRUE,FALSE,TRUE,TRUE),
x3 = matrix(1:6,2,3),
x4 = list(x41 = c(1,2), x42 = c(3,4)) )
lapply(x, mean) # áp dụng hàm mean trên tất cả các phần tử con của x## $x1
## [1] 5.5
##
## $x2
## [1] 0.75
##
## $x3
## [1] 3.5
##
## $x4
## [1] NA
Kết quả của lapply() là một list có số lượng phần tử và tên các phần tử con giống với véc-tơ \(x\). Trong trường hợp áp dụng hàm mean(), mỗi giá trị nằm trong \(list\) kết quả là giá trị trung bình của phần tử có tên tương ứng nằm trong \(list\) ban đầu. Do hàm mean() không thể sử dụng với một \(list\) nên phần tử \(x4\) của kết quả nhận giá trị \(NA\).
Hàm sapply() có cơ chế hoạt động hoàn toàn tương tự như lapply() và chỉ khác ở chỗ kết quả đầu ra là dưới dạng véc-tơ, ma trận, hoặc mảng. Thật vậy, vẫn với đối tượng \(x\) kiểu \(list\) ở trên, chúng ta sử dụng sapply() thay vì lapply() sẽ cho kết quả dưới dạng véc-tơ thay vì dưới dạng \(list\)
sapply(x, mean) # áp dụng hàm mean trên tất cả các phần tử con của x## x1 x2 x3 x4
## 5.50 0.75 3.50 NA
Các hàm lapply() và sapply() thường xuyên được sử dụng khi làm việc với dữ liệu vì R lưu dữ liệu dưới dạng các \(data.frame\) hoặc \(tibbles\). Khi sử dụng các hàm lapply() và sapply() với dữ liệu, các đối tượng được tác động đến sẽ luôn luôn là các véc-tơ cột. Hãy quan sát ví dụ dưới đây khi sử dụng sapply() để tính tỷ lệ giá trị không quan sát được của mỗi véc-tơ cột của một dữ liệu có tên là \(gapminder\) nằm trong thư viện \(dslabs\).
library(dslabs)
s1<-sapply(gapminder, function(x) sum(is.na(x))/length(x)) # tỷ lệ không quan sát được của mỗi cột trong dữ liệu
s1<-sort(s1) # sắp xếp s1 theo thứ tự tăng dần
print(s1) # hiển thị s1## country year life_expectancy continent
## 0.00000000 0.00000000 0.00000000 0.00000000
## region population fertility infant_mortality
## 0.00000000 0.01754386 0.01773352 0.13779042
## gdp
## 0.28183973
barplot(s1,col = "lightskyblue",
ylab = "Tỷ lệ",
xlab = "Tên biến/cột",
main = "Tỷ lệ missing value của các cột dữ liệu Gapminder")
2.5 Phụ lục
2.5.1 Kiến thức nâng cao liên quan đến ma trận
Các kiến thức liên quan đến ma trận trong phần này đòi hỏi bạn đọc cần có kiến thức nâng cao hơn. Nếu bạn đọc cảm thấy không cần thiết có thể bỏ qua vì các kiến thức được sử dụng trong phần này sẽ được nhắc lại khi có ứng dụng cụ thể.
2.5.2 Các giá trị đặc trưng của một dòng tiền tương lai
2.5.2.1 Giá trị hiện tại và tỷ suất sinh lời nội bộ
Giá trị hiện tại của một dòng tiền \(CF\) với lãi suất \(i\) tính theo kiểu gộp được tính như sau \[\begin{align} PV(CF) = \sum\limits_{t=1}^n \ \cfrac{CF_t}{(1+i)^t} \end{align}\]
Giá trị hiện tại của một dòng tiền là một thước đo cho giá trị của tài sản tạo ra dòng tiền đó. Nhìn chung, tài sản nào có giá trị hiện tại lớn hơn thì có giá trị cao hơn.
Tỷ suất sinh lời nội bộ (Internal rate of return hay IRR) là mức lãi suất \(i_0\) mà tính theo mức lãi suất này giá trị hiện tại của một dòng tiền bằng 0. Tỷ suất sinh lời nội bộ không tồn tại nếu dòng tiền tương lai của một tài sản chỉ có giá trị dương (hoặc âm).
2.5.2.2 Durations và convexity
Duration không phải là thước đo thời gian từ lúc bắt đầu đến lúc đáo hạn của dòng tiền mà là một thước đo cho sự nhạy cảm của giá trị hiện tại của dòng tiền theo sự thay đổi của lãi suất.
##
## Attaching package: 'dplyr'
## The following object is masked from 'package:gridExtra':
##
## combine
## The following object is masked from 'package:kableExtra':
##
## group_rows
## The following objects are masked from 'package:stats':
##
## filter, lag
## The following objects are masked from 'package:base':
##
## intersect, setdiff, setequal, union
##
## Attaching package: 'lubridate'
## The following objects are masked from 'package:base':
##
## date, intersect, setdiff, union
3 Phân tích dữ liệu bằng R
Trong phần này của cuốn sách, bạn sẽ tìm hiểu về các kỹ thuật phân tích dữ liệu, bao gồm có tiền xử lý dữ liệu, sắp xếp dữ liệu và trực quan hóa dữ liệu.
Tiền xử lý dữ liệu bao gồm tất cả các kỹ thuật đưa dữ liệu từ các nguồn khác nhau vào R và biến đổi thành định dạng để có thể làm việc được.
Sắp xếp dữ liệu bao gồm các bước biến đổi, chuyển hóa dữ liệu thành định dạnh để có thể trực quan hóa, phân tích, và xây dựng mô hình.
Trực quan hóa dữ liệu là một nghệ thuật biến đổi dữ liệu dưới dạng các con số, chuỗi ký tự,…, thành các biểu đồ, đồ thị hay hình ảnh sử dụng các hình dạng, màu sắc, khoảng cách để con người dễ dàng nhận thức và hiểu về dữ liệu. Trực quan hóa dữ liệu còn có thể giúp người phân tích tìm ra những giá trị ẩn chứa trong dữ liệu.
4 Nhập dữ liệu từ các nguồn khác nhau vào R
4.1 Đối tượng dùng để lưu dữ liệu trong R
Hai kiểu đối tượng thường được dùng để lưu dữ liệu trong R là \(data.frame\) và \(tibble\). Chúng ta sẽ thảo luận về \(data.frame\) trước vì đây là kiểu lưu dữ liệu phổ biến. Kiểu \(tibble\) với một vài ưu điểm hơn \(data.frame\) sẽ được thảo luận trong phần tiếp theo.
4.1.1 \(data.frame\) là gì?
\(data.frame\) là đối tượng phổ biến nhất để lưu trữ dữ liệu trên cửa sổ làm việc của R. Hiểu một cách đơn giản, một \(data.frame\) là một bảng excel mà mỗi cột tương ứng với một véc-tơ và mỗi dòng tương ứng với một quan sát. Ngay khi cài đặt R, đã có nhiều đối tượng là dữ liệu kiểu \(data.frame\) đã được lưu trữ trong R và sẵn sàng sử dụng mà không cần gọi thư viện bổ sung. Để biết trên cửa sổ Rstudio đang sử dụng có những dữ liệu nào, bạn đọc sử dụng câu lệnh data()
data()Bạn đọc có thể thấy trên cửa sổ R Script xuất hiện một cửa sổ mới với danh sách tất cả các dữ liệu sẵn có trong R và dữ liệu sẵn có trong các thư viện được cài đặt thêm mà bạn đọc đang gọi ra trên cửa sổ làm việc. Để biết trong một thư viện đang được gọi ra trên cửa sổ Rstudio có những dữ liệu nào, bạn đọc có thể sử dụng lệnh data() kèm với tùy chọn \(package\)
library(dslabs) # gọi thư viện dslabs lên trên màn hình
data(package = "dslabs") # cho biết có những data nào trong thư viện dslabsTrong danh sách dữ liệu của thư viện \(dslabs\), bạn đọc có thể thấy một đối tượng có tên \(murders\). Đây là một \(data.frame\). Bạn đọc có thể kiểm tra kiểu của đối tượng này bằng hàm class()
class(murders) # trên màn hình console sẽ cho biết đây là một data frame## [1] "data.frame"
Thông thường để có hiểu biết ban đầu về một đối tượng kiểu \(data.frame\), bạn đọc nên bắt đầu bằng đọc mô tả về dữ liệu (nếu có) bằng cách sử dụng ?
? murders # trên cửa sổ help sẽ hiển thị mô tả về murdersNhóm các câu lệnh dưới đây giúp bạn đọc hiểu được cấu trúc của dữ liệu trong \(data.frame\) đó
View(murders) # Hiển thị data.frame dưới dạng bảng
head(murders,k = 5) # Hiển thị k dòng đầu tiên của data.frame## state abb region population total
## 1 Alabama AL South 4779736 135
## 2 Alaska AK West 710231 19
## 3 Arizona AZ West 6392017 232
## 4 Arkansas AR South 2915918 93
## 5 California CA West 37253956 1257
## 6 Colorado CO West 5029196 65
str(murders) # Hiển thị cấu trúc của data.frame.## 'data.frame': 51 obs. of 5 variables:
## $ state : chr "Alabama" "Alaska" "Arizona" "Arkansas" ...
## $ abb : chr "AL" "AK" "AZ" "AR" ...
## $ region : Factor w/ 4 levels "Northeast","South",..: 2 4 4 2 4 4 1 2 2 2 ...
## $ population: num 4779736 710231 6392017 2915918 37253956 ...
## $ total : num 135 19 232 93 1257 ...
Hàm
head()hiển thị nhanh các dòng đầu tiên của dữ liệu cho bạn đọc cái nhìn ban đầu, tuy nhiên hàmhead()không hiệu quả khi dữ liệu có nhiều cột.Hàm
View()cho hiển thị về dữ liệu dễ nhìn nhất. HàmView()có hạn chế khi dữ liệu có quá nhiều dòng hoặc nhiều cột và thời gian hiển thị lâu hơn so vớihead().Hàm
str()là cách hiển thị dữ liệu một cách tổng quát và hiệu quả hơn so vớihead()hoặcView(). Kết quả từ hàmstr()với dữ liệu \(murders\) cho thấy đây là một dữ liệu dạng bảng với 5 cột (5 variables) và 51 dòng (51 observations). Ngoài ra, sử dụng hàmstr()bạn đọc có thể thấy được kiểu dữ liệu của từng cột; chẳng hạn như cột \(state\) là cột chứa dữ liệu kiểu \(character\); cột \(region\) có kiểu dữ liệu là \(factor\),…
Một hàm số hiệu quả khác thường được sử dụng để bạn đọc có cái nhìn tổng quan về dữ liệu là hàm summary(). Chúng ta có thể quan sát kết quả khi sử dụng hàm `summary() với dữ liệu \(murders\) như sau
summary(murders) # in ra màn hình cột population của data.frame murders## state abb region population
## Length:51 Length:51 Northeast : 9 Min. : 563626
## Class :character Class :character South :17 1st Qu.: 1696962
## Mode :character Mode :character North Central:12 Median : 4339367
## West :13 Mean : 6075769
## 3rd Qu.: 6636084
## Max. :37253956
## total
## Min. : 2.0
## 1st Qu.: 24.5
## Median : 97.0
## Mean : 184.4
## 3rd Qu.: 268.0
## Max. :1257.0
Hàm summary() cho biết thông chi tiết hơn về giá trị trong mỗi cột.
Cột \(state\) và cột \(abb\) là cột mà giá trị trong đó là kiểu \(character\)
Cột \(region\) là kiểu factor, có thể nhận một trong bốn giá trị là \(Northeast\), \(South\), \(North\) \(Central\), hoặc \(West\) và cho biết mỗi giá trị xuất hiện bao nhiêu lần trong cột dữ liệu.
Các cột \(population\) và \(total\) là các cột kiểu số. Chúng ta có thể thấy các giá trị lớn nhất, nhỏ nhất, giá trị trung bình và các giá trị tứ phân vị. Bạn đọc có thể hình dung ra phân phối của các giá trị trong cột giá trị kiểu số.
Trong trường hợp cột có giá trị không quan sát được, hàm
summary()cũng sẽ cho biết có bao nhiêu giá trị này trong mỗi cột.
Để lấy ra một cột dữ liệu của một \(data.frame\) chúng ta sử dụng \(\$\). Chẳng hạn như để lấy giá trị cột \(population\) của dữ liệu \(murders\):
murders$population # in ra màn hình cột population của data.frame murdersNhư đã nói ở trên, kiểu dữ liệu của cột \(region\) là kiểu \(factor\). Về bản chất, véc-tơ kiểu \(factor\) là một véc-tơ kiểu chuỗi ký tự nhưng được lưu theo một cách hiệu quả hơn, tiết kiệm bộ nhớ, và thuận lợi cho người sử dụng khi phân tích dữ liệu.
- Dữ liệu kiểu factor sẽ lưu véc-tơ chuỗi ký tự dưới dạng vec-tơ số tự nhiên và mỗi chuỗi ký tự sẽ được cho tương ứng với một số tự nhiên. Các lưu này hiệu quả hơn về bộ nhớ khi làm việc với các véc-tơ kiểu chuỗi ký tự nếu có nhiều chuỗi ký tự bị lặp lại trong véc-tơ. Để biết một vec-tơ dạng factor có bao nhiêu giá trị riêng biệt, mỗi giá trị riêng biệt được cho tương ứng với số tự nhiên nào, và mỗi giá trị riêng biệt được lặp lại bao nhiêu lần trong véc-tơ, bạn đọc sử dụng hàm
summary()hoặc hàmtable()
# summary(murders$region) # Tổng hợp thông tin của vec-tơ dạng factor
table(murders$region) # cho kết quả tương tự như summary##
## Northeast South North Central West
## 9 17 12 13
Kết quả từ hàm table() cho thấy cột \(region\) có 4 giá trị; cách cho tương ứng mỗi chuỗi ký tự với các số lần lượt là \(Northeast \rightarrow 1\) ; \(South \rightarrow 2\); \(North Central \rightarrow 3\), và \(West \rightarrow 4\); tần suất xuất hiện của mỗi giá trị cũng được cho trong bảng: có 9 giá trị \(Northeast\), có 17 giá trị \(South\), có 12 giá trị \(North Central\), và 13 giá trị \(West\).
- Khi lưu dữ liệu kiểu factor thay vì chuỗi ký tự nghĩa là bạn đọc đang định nghĩa dữ liệu là kiểu biến rời rạc (categorial variable). Biến có thể trực tiếp đưa vào các mô hình và không cần thực hiện thêm biến đổi nào khác.
Trong hầu hết các trường hợp bạn đọc sẽ dùng R để xử lý dữ liệu từ nguồn ngoài vào. Chúng ta sẽ sử dụng các hàm có sẵn trong R đọc dữ liệu và kết quả đầu ra của hàm này sẽ là các \(data.frame\). Trong một vài trường hợp, bạn đọc sẽ phải tự tạo \(data.frame\). Câu lệnh để tạo một \(data.frame\) (tên \(df\)) với các cột có tên lần lượt là \(id\), \(names\), \(grades\), và \(result\) được viết như sau
df<-data.frame( # hàm data.frame() dùng để tạo data.frame tên df
id = paste("SV",1:5), # cột có tên là ID nhận giá trị "SV1",...,"SV5"
names = c("You", "Me", "Him", "Her", "John"), # Cột names
grades = c(5.5, 1.5, 10.0, 9.0, 7.6), # Cột grades
result = c(TRUE, FALSE,TRUE, TRUE, TRUE)) # Cột resultĐối tượng kiểu \(data.frame\) có một vài nhược điểm khi sử dụng để lưu dữ liệu từ các nguồn khác nhau vào R. Do đó kiểu đối tượng mới được phát triển để khắc phục các nhược điểm này, đó là \(tibble\). Phần tiếp theo chúng ta sẽ thảo luận về đối tượng này.
4.1.2 \(tibble\) là một cải tiến của \(data.frame\)?
Về cơ bản một \(tibble\) là cũng có thể hiểu là một \(data.frame\) với một vài điều chỉnh để giúp việc lấy dữ liệu từ nguồn bên ngoài vào phân tích trở nên dễ dàng hơn. Ở mức độ phân tích dữ liệu thông thường, sự khác khác nhau giữa \(tibble\) và \(data.frame\) là không đáng kể. Nếu cần liệt kê ra sự khác nhau cơ bản giữu hai đối tượng này thì có thể kể đến:
- Thứ nhất: khi in một \(tibble\) ra màn hình sẽ chỉ có 10 dòng đầu được hiển thị và số lượng cột của một \(tibble\) luôn luôn khớp với kích thước cửa sổ R Console, đồng thời kiểu dữ liệu của mỗi cột sẽ được hiển thị ngay dưới tên cột
library(tibble)
trump_tweets # in một data frame ra màn hình sẽ không hiệu quả
# Hàm as_tibble đổi data.frame sang tibble
as_tibble(trump_tweets) # Hiển thị 1 tibble hiệu quả hơn.Thứ hai: khi lấy dữ liệu từ bên ngoài vào trong R, \(tibble\) không đổi tên cột dù tên cột không phải là kiểu tên được phép trong R. Đồng thời, khi tạo một \(tibble\), bạn đọc có thể đặt tên cột là một kiểu tên không được phép sử dụng với tên biến thông thường.
Cuối cùng, khi dữ liệu từ bên ngoài được lưu vào một \(tibble\), kiểu dữ liệu sẽ không thay đổi.
Để tạo một \(tibble\), bạn đọc có thể sử dụng hàm tibble(). Bạn đọc có thể tạo ra một dữ liệu có 3 cột mà tên các cột đều không thể được sử dụng làm tên biến trong R như sau
tib<-tibble( # hàm tibble dùng để tạo tibble
":D" = c(1,2,3), # có thể dùng tên cột là ":D"
":p" = c("X","Y","Z"), # có thể dùng tên cột là ":p"
"1" = 2 # có thể dùng tên cột là "1"
)
tib## # A tibble: 3 × 3
## `:D` `:p` `1`
## <dbl> <chr> <dbl>
## 1 1 X 2
## 2 2 Y 2
## 3 3 Z 2
Nếu thay thế đoạn lệnh trên bằng hàm data.frame() thì hàm \(data.frame\) được tạo thành sẽ tự động thay đổi tên cột
df<-data.frame( # tạo data.frame thay vì tibble
":D" = c(1,2,3), # data.frame sẽ đổi tên cột cho phù hợp
":p" = c("X","Y","Z"), # data.frame sẽ đổi tên cột cho phù hợp
"1" = 2 # data.frame sẽ đổi tên cột cho phù hợp
)
df # hãy quan sát xem tên cột của df thay đổi như thế nào## X.D X.p X1
## 1 1 X 2
## 2 2 Y 2
## 3 3 Z 2
Bạn đọc có thể thấy rằng dữ liệu có tên \(df\) nếu được lưu dưới dạng \(data.frame\) thì tên các cột đã được tự động thay đổi cho thích hợp với tên biến. Những điểm khác nhau giữa \(tibble\) và \(data.frame\) sẽ được tiếp tục thảo luận ở các phần tiếp theo khi chúng tôi giới thiệu về các hàm dùng để nhập dữ liệu vào R.
4.1.3 Nhập dữ liệu bằng hàm sẵn có.
Danh sách các hàm sẵn có trong R và kiểu dữ liệu tương ứng có thể nhập được liệt kê trong các bảng sau:
| Hàm số | Sử dụng trong trường hợp |
|---|---|
| read.table() | Đọc các file có đuôi dạng .txt |
| read.csv() | file dạng csv mà giá trị được ngăn cách bằng dấu ‘,’ |
| read.csv2() | file dạng csv mà giá trị được ngăn cách bằng dấu ‘;’ |
| read.delim() | Các file dạng text, các giá trị cách nhau bởi ký tự mà bạn đọc định nghĩa |
| readRDS | Dữ liệu được lưu dưới dạng .rds |
Khi lấy dữ liệu từ các nguồn bên ngoài vào bằng các câu lệnh có sẵn, tên của các cột dữ liệu có thể bị thay đổi do một số tên cột không thể được dùng để đặt tên của \(data.frame\). Do đó, bạn đọc hãy luôn kiểm tra lại tên các cột dữ liệu sau khi đọc. Hàm names() cho biết tên các cột của một \(data.frame\).
df<-read.csv(header = TRUE,
text = "@1,@2
1,2
3,4") # sử dụng read.csv để đọc đoạn text
names(df) # hiển thị tên của các cột## [1] "X.1" "X.2"
Để đổi tên của \(data.frame\) ở trên, bạn đọc cần gán \(names(df)\) bằng một véc-tơ chứa tên các cột. Hãy đảm bảo rằng độ dài của vec-tơ chứa tên cột bằng số cột của \(data.frame\)
## @1 @2
## 1 1 2
## 2 3 4
4.1.4 Nhập dữ liệu bằng thư viện \(readr\).
Các câu lệnh để đọc dữ liệu của thư viện \(readr\) tương tự như các câu lệnh sẵn có, nhưng đặc biệt hiệu quả hơn về thời gian đọc dữ liệu. Hàm số dùng để đọc các file định dạng \(csv\) trong thư viện \(readr\) là hàm read_csv(). Để so sánh thời gian đọc dữ liệu vào R của hàm read_csv() và hàm read.csv() chúng ta sẽ tạo hai file dữ liệu bao gồm test1.csv và test2.csv
x<-matrix(rnorm(10^6),10^2,10^4) # tạo thành 1 ma trận 100 hàng, 10^4 cột
write.csv(x,"test1.csv") # lưu ma tran thanh file .csv
x<-matrix(rnorm(10^7),10^2,10^5) # tạo thành 1 ma trận 100 hàng, 10^5 cột
write.csv(x,"test2.csv") # lưu ma tran thanh file .csvBạn đọc có thể kiểm tra kích thước của các file test1.csv và test2.csv lần lượt là khoảng 18 Mega byte và 180 Mega byte. Chúng ta sẽ kiểm tra thời gian mà các hàm read.csv() và read_csv() nhập dữ liệu đối với dữ liệu test1.csv trước:
start<-proc.time() # lưu lại thời điểm trước khi chạy read.csv
dat<-read.csv("test1.csv") # dùng hàm read.csv để load dữ liệu
proc.time() - start # tính thời gian hàm read.csv chạy
start<-proc.time() # lưu lại thời điểm trước khi chạy read_csv
dat<-read_csv("test1.csv") # dùng hàm read_csv để load dữ liệu
proc.time() - start # tính thời gian hàm read_csv chạyĐối với dữ liệu \(test1.csv\) thì thời gian nhập dữ liệu của read_csv() có nhanh hơn nhưng không có sự khác biệt đáng kể. Tuy nhiên sự khác biệt sẽ rõ ràng khi nhập dữ liệu \(test2.csv\). Bạn đọc cân nhắc khi dùng hàm read.csv() đọc dữ liệu thời gian nhập dữ liệu có thể lên đến hơn 20 phút.
start<-proc.time() # lưu lại thời điểm trước khi chạy read.csv
dat<-read.csv("test2.csv") # !!! THỜI GIAN CHẠY CÓ THỂ LÊN ĐẾN 20-25 phút
proc.time() - start # tính thời gian hàm read.csv chạy
start<-proc.time() # lưu lại thời điểm trước khi chạy read_csv
dat<-read_csv("test2.csv") # dùng hàm read_csv để load dữ liệu
proc.time() - start # tính thời gian hàm read_csv chạyHàm read_csv() sẽ mất khoảng 2 phút để đọc dữ liệu \(test2.csv\), nghĩa là thời gian tiết kiêm lên đến hơn 10 lần! Danh sách các hàm để đọc dữ liệu trong gói lệnh \(readr\) như sau
| Hàm số | Sử dụng trong trường hợp |
|---|---|
| read_csv() | file dạng csv mà giá trị được ngăn cách bằng dấu ‘,’ |
| read_csv2() | file dạng csv mà giá trị được ngăn cách bằng dấu ‘;’ |
| read_tsv() | Các file dạng text mà các giá trị cách nhau bởi khoảng trống |
| read_delim() | Các file dạng text mà các giá trị cách nhau bởi ký tự bất kỳ |
Một sự khác biệt cơ bản khác của các hàm đọc dữ liệu trong readr đó là dữ liệu được lưu vào một Tibble thay vì một Data frame. Điều này giúp cho dữ liệu không bị thay đổi định dạng và giữ nguyên tên cột. Các lưu ý khác khi bạn đọc sử dụng các hàm số đọc dữ liệu của readr
- Các hàm số trong readr luôn hiểu hàng đầu tiên của dữ liệu là tên của mỗi cột. Do đó, bạn đọc cần sử dụng tham số \(col_names = FALSE\) nếu không muốn readr hiểu hàng đầu tiên là tên của mỗi cột dữ liệu.
library(readr)
# Kết quả sẽ là một Tibble 1 hàng và 3 cột
read_csv("1,2,3
4,5,6") # tên các cột là "1", "2", và "3"## # A tibble: 1 × 3
## `1` `2` `3`
## <dbl> <dbl> <dbl>
## 1 4 5 6
# Kết quả sẽ là một Tibble 2 hàng và 3 cột
read_csv("1,2,3
4,5,6", col_names = FALSE) # readr tự động đặt tên các cột X1, X2, X3## # A tibble: 2 × 3
## X1 X2 X3
## <dbl> <dbl> <dbl>
## 1 1 2 3
## 2 4 5 6
- Trong nhiều file dữ liệu các hàng đầu tiên là các mô tả về dữ liệu nên khi sử dụng readr, bạn đọc có thể sử dụng tùy biến \(skip = k\) để loại bỏ \(k\) dòng đầu tiên trong file dữ liệu.
# Kết quả sẽ là một Tibble 2 hàng và 3 cột
read_csv("Trường ĐHKTQD
Khoa toán Kinh tế
1,2,3
4,5,6", col_names = FALSE, skip = 2) # readr sẽ không đọc 2 dòng đầu## Rows: 2 Columns: 3
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## dbl (3): X1, X2, X3
##
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
## # A tibble: 2 × 3
## X1 X2 X3
## <dbl> <dbl> <dbl>
## 1 1 2 3
## 2 4 5 6
Bạn đọc cũng có thể sử dụng tham số \(col_names\) để gán giá trị cho tên các cột, tuy nhiên lời khuyên của chúng tôi là bạn đọc hãy đặt tên cho các cột bằng hàm \(names()\) sau khi lưu dữ liệu vào tibble để tránh sự phức tạp không đáng có.
Cách sử dụng các hàm khác ngoài read_csv() bạn đọc có thể tham khảo trong hướng dẫn của gói lệnh \(readr\). Sau khi đọc qua hướng dẫn, bạn đọc hãy thử kiểm tra xem các câu lệnh sau có vấn đề gì và nếu có thể, bạn đọc hãy thử lựa chọn hàm hoặc thêm tham số phù hợp để đọc dữ liệu
4.1.5 Tương tác giữa R và Microsoft Excel
4.1.5.1 Đọc dữ liệu lưu dưới định dạng của Excel
Microsoft Excel là rất phổ biến trong môi trường làm việc công sở. Do đó, dữ liệu nhận sẽ có thể là các file định dạng (.xls, .xlsx, .xlsb, .xlsm … ). Các gói lệnh \(openxlsx\) và \(readxl\) thường được sử dụng để đọc dữ liệu từ các file có định dạng như vậy.
4.1.6 Lấy dữ liệu từ một hệ cơ sở dữ liệu
Có thể sử dụng R như một công cụ để kết nối và thực hiện các câu lệnh truy vấn vào các cơ sở dữ liệu. Để làm được điều này, trước tiên, bạn đọc cần phải cài đặt một Open Database Connectivity (ODBC), được gọi là kết nối cơ sở dữ liệu mở. Kết nối này giúp cho hệ điều hành máy tính tương thích với hệ quản lý cơ sở dữ liệu mà bạn sử dụng. Nhóm tác giả sử dụng hệ điều hành Windows và hệ quản trị cơ sở dữ liệu MySQL nên chúng tôi sẽ lựa chọn ODBC phù hợp. Bạn đọc tham khảo tại địa chỉ https://dev.mysql.com/downloads/connector/odbc/. Tại thời điểm nhóm tác giả viết cuốn sách này ODBC cho hệ điều hành Windows đang là phiên bản 8.0
Sau khi cài đặt ODBC lên hệ điều hành, bạn đọc đã có thể sử dụng R để truy cập vào một cơ sở dữ liệu và thực hiện các câu lệnh truy vấn dữ liệu trên cơ sở dữ liệu đó trên R với sự trợ giúp của thư viện \(DBI\). Sau khi cài đặt thư viện \(DBI\), bạn đọc cần tạo một kết nối giữa R và cơ sở dữ liệu bằng hàm \(dbConnect\).
library(DBI)
con <- dbConnect(odbc::odbc(), .connection_string = "Driver={MySQL ODBC 8.0 Unicode Driver};",
Server = "ten_serve", Database = "db_name", UID = "ID", PWD = "password")Trong đó “ten_serve” là địa chỉ local hoặc server lưu trữ cơ sở dữ liệu, “db_name” là tên của cơ sở dữ liệu, ID và password lần lượt là tên và password mà bạn đọc tự tạo hoặc được cấp để truy cập vào cơ sở dữ liệu. Sau khi đã tạo được kết nối, bạn đọc có thể thực hiện bất kỳ câu lệnh truy vấn dữ liệu nào từ R với hàm “DBI::dbGetQuery()”. Chẳng hạn, bạn đọc muốn lấy ra thông tin của tất cả những người có ngày sinh là ngày 01 tháng 01 năm 2000 từ một bảng có tên là Life_Insured từ một cơ sở dữ liệu tên là \(tktdb\), bạn đọc thực hiện như sau
sql<-"select * from tktdb.Life_Insured
where DOB = '2000-01-01'" # Viết đúng câu lệnh truy vấn từ MySQL
df<-DBI::dbGetQuery(sql) # data.frame df sẽ lưu kết quả của câu lệnh truy vấnTIP: Khi làm việc với dữ liệu được trích xuất từ một hệ cơ sở dữ liệu, bạn đọc hãy cố gắng thực hiện các phép biến đổi, sắp xếp dữ liệu bằng SQL thay vì thực hiện biến đổi trên R vì các hệ quản trị cơ sở dữ liệu thực hiện các chức năng này nhanh hơn R rất nhiều.
5 Tiền xử lý dữ liệu
Tiền xử lý dữ liệu là một công việc đòi hỏi sự tỉ mỉ, cẩn thận và cũng là một trong những bước quan trọng nhất trong một quy trình làm việc với dữ liệu. Tiền xử lý dữ liệu là tập hợp tất cả các bước kỹ thuật nhằm đảm bảo cho dữ liệu bạn sử dụng để phân tích được đảm bảo về định dạng, giá trị và ý nghĩa. Hiểu một cách đơn giản, tiền xử lý dữ liệu là biến dữ liệu thô thành dữ liệu có thể sử dụng được để phân tích và đưa ra kết quả.
Khi làm việc với dữ liệu, thực tế là đến hơn 50% các trường hợp bạn đọc sẽ nhận được những dự liệu dạng thô chưa qua xử lý. Nếu những dữ liệu này được nhập và xuất ra qua một hệ thống được phát triển đầy đủ, tiền xử lý dữ liệu chỉ cần qua một vài bước cơ bản để đi đến kết quả. Trong trường hợp dữ liệu bạn nhận được là dữ liệu được nhập một cách thủ công, thông qua nhiều người nhập thì đây thực sự sẽ là một vấn đề lớn. Tiền xử lý dữ liệu trong hoàn cảnh như vậy có thể chiếm 80% - 90% thời gian công việc của bạn!.
5.1 Tiền xử lý dữ liệu là gì?
Các vấn đề thường gặp phải khi làm việc với một dữ liệu từ các nguồn khác nhau thường xuất phát từ hai vấn đề
Dữ liệu sai định dạng: trong cùng một cột dữ liệu có các biến kiểu khác nhau hoặc kiểu của biến không đúng như quy ước.
Dữ liệu chứa giá trị không quan sát được hoặc chứa các giá trị ngoại lai (outliers).
Ví dụ như bạn đọc nhận được dữ liệu về 3 ứng cử viên từ bộ phận nhân sự như sau với yêu cầu về cho biết độ tuổi trung bình của những ứng cử viên và tỷ lệ Nam/Nữ ứng tuyển
| Họ và tên | Ngày sinh | Giới tính |
|---|---|---|
| Nguyễn Văn An | 01/02/98 | Nam |
| Trần Văn Cường | 12/17/1999 | NA |
| Lê Thị Loan | 1-1-1992 | NA |
Đây là một dữ liệu không thể sử dụng để phân tích được bởi vì giá trị trong các cột ngày sinh là không đúng định dạng ngày tháng đồng thời có các giá trị không quan sát được ở cột giới tính. Nếu sử dụng dữ liệu này để phân tích mà bỏ qua việc tiền xử lý dữ liệu thì kết quả sẽ sai lệch hoàn toàn so với bản chất của dữ liệu:
Tính độ tuổi trung bình của các ứng cử viên là không thể thực hiện được với dữ liệu này bởi vì cột ngày sinh đang là dạng chuỗi ký tự và định dạng ngày tháng là rất lộn xộn
Nếu bỏ qua những giá trị không có quan sát, tỷ lệ giới tính Nam là 100%. Liệu con số này có thực sự đúng?
Tiền xử lý dữ liệu không chỉ bao gồm các công cụ kỹ thuật mà còn yêu cầu cả kiến thức phổ thông và kiến thức nghiệp vụ của người làm dữ liệu. Khi có vấn đề gây khó hiểu về dữ liệu nhận được, điều hết cần làm đó là liên hệ với người chủ dữ liệu để kiểm tra lại thông tin. Khi việc này là không thể thực hiện được, người xử lý dữ liệu sẽ phải đưa ra các phán đoán về dữ liệu đó dựa trên hiểu biết của mình.
Với cột ngày sinh của các nhân viên:
Giá trị “01/02/98” có khả năng cao là ngày 01 tháng 02 năm 1998 do quy ước phổ biến ở Việt Nam là viết theo thứ tự ngày -> tháng -> năm.
Giá trị “12/17/1999” có khả năng cao là ngày 17 tháng 12 năm 1999. Khi gặp các trường hợp này nhiều khả năng người nhập dữ liệu sử dụng format ngày tháng của Microsoft Excel.
Giá trị “1-1-1992” có khả năng cao là ngày 01 tháng 01 năm 1992.
Như vậy với mỗi giá trị trong cột ngày sinh, bạn đọc cần một phép biến đổi khác nhau
DOB<-rep(as.Date("1900-01-01"),3) # tạo vector dạng date độ dài 3
DOB[1]<-as.Date("01/02/98", format = "%d/%m/%y")
DOB[2]<-as.Date("12/17/1999", format = "%m/%d/%Y")
DOB[3]<-as.Date("1-1-1992", format = "%d-%m-%Y")Vec-tơ \(DOB\) chứa giá trị ngày sinh dạng ngày tháng của các ứng cử viên và bạn đọc đã có thể sử dụng các hàm số có sẵn để tính tuổi của các ứng cử viên.
Với cột giới tính của nhân viên:
Giới tính của ứng cử viên Trần Văn Cường là không quan sát được tuy nhiên theo tên của ứng cử viên thì nhiều khả năng đây là Nam.
Giới tính của ứng cử viên Lê Thị Loan là không quan sát được tuy nhiên theo tên của ứng cử viên thì nhiều khả năng đây là Nữ.
Sau những bước xử lý như trên, chúng ta đã có một dữ liệu được định dạng chính xác và đã có thể đưa ra các phân tích về dữ liệu
| Họ và tên | Ngày sinh | Giới tính |
|---|---|---|
| Nguyễn Văn An | 1998-02-01 | Nam |
| Trần Văn Cường | 1999-12-17 | Nam |
| Lê Thị Loan | 1992-01-01 | Nữ |
Đây là một ví dụ điển hình của tiền xử lý dữ liệu. Dữ liệu bạn đọc nhận được sẽ hiếm khi được định dạng chuẩn và sẵn sàng để phân tích giống như những dữ liệu sẵn có trên R. Để xử lý những giá trị sai định dạng, và điền vào dữ liệu các giá trị không quan sát, loại bỏ các giá trị ngoại lai, …, người làm dữ liệu phải sử dụng kiến thức phổ thông và kiến thức nghiệp vụ để dưa ra dự đoán tốt nhất có thể.
5.2 Định dạng lại các cột dữ liệu sử dụng thư viện \(readr\)
5.2.1 Quy tắc định đạng cột của \(readr\)
Mỗi thư viện đọc dữ liệu sẽ có các quy tắc khác nhau khi đọc và định dạng lại dữ liệu khi lưu trên R. Chúng ta sẽ tập trung vào cách thư viện “readr” đọc dữ liệu. Khi đọc một file vào R, các hàm đọc dữ liệu của thư viện \(readr\) cố gắng dự đoán kiểu dữ liệu của từng cột bằng cách sử dụng 1000 hàng dữ liệu đầu tiên dựa trên nguyên tắc như sau:
| Giá trị trong cột dữ liệu | \(readr\) dự đoán |
|---|---|
| Cột dữ liệu chỉ bao gồm TRUE, FALSE, True, False, true, false, F, T, t, f | Kiểu logic |
| Cột dữ liệu chỉ bao gồm số, số thập phân sử dụng dấu ‘.’ , | Kiểu số |
| Lê Thị Loan | 1-1-1992 |
Lưu ý rằng các giá trị không quan sát được không ảnh hưởng đến việc \(readr\) dự đoán kiểu dữ liệu của một cột. Khi đọc dữ liệu kiểu số, việc sử dụng dấu thập phân là ‘.’ (dữ liệu từ các nước sử dụng ngôn ngữ tiếng Anh) hay ‘,’ (dữ liệu từ Việt Nam, Pháp) có thể làm cho giá trị của biến kiểu số thay đổi về bản chất. Bạn đọc không sử dụng tham số nào khác khi sử dụng \(readr\), số thập phân luôn được hiểu là ‘.’ khi bạn đọc sử dụng read_csv() và số thập phân sẽ là ‘,’ nếu bạn đọc dùng read_csv2().
file<-"C1;C2;C3;C4; C5
1e-10;2.2;1.0;TRUE; 1.0.0.0
Inf;3,2;1,000.0;1;10%"
read_csv2(file)## # A tibble: 2 × 5
## C1 C2 C3 C4 C5
## <dbl> <dbl> <dbl> <chr> <chr>
## 1 1e-10 22 10 TRUE 1.0.0.0
## 2 Inf 3.2 1 1 10%
Từ kết quả trên có thể thấy rằng \(readr\) sẽ bỏ qua dấu ‘,’ trong các số khi bạn đọc dùng read_csv() và bỏ qua ‘.’ với biến kiểu số khi bạn đọc dùng read_csv2(). Đối với biến kiểu logic, bạn đọc có thể kiểm tra kiểu dữ liệu của các cột dưới đây đều là kiểu logic sau khi dữ liệu được đọc bằng read_csv()
file<-"X1,X2,X3,X4,X5,X6
TRUE,t,True,false,F,
F,F,FALSE,T,f,True"
read_csv(file)## # A tibble: 2 × 6
## X1 X2 X3 X4 X5 X6
## <lgl> <lgl> <lgl> <lgl> <lgl> <lgl>
## 1 TRUE TRUE TRUE FALSE FALSE NA
## 2 FALSE FALSE FALSE TRUE FALSE TRUE
Để hiểu kỹ hơn cách \(readr\) dự đoán kiểu giá trị trong cột, bạn đọc có thể đọc hướng dẫn của hàm guess_parse().
5.2.2 Định dạng cột bằng các hàm parse_*()
Với các cột mà không thể xác định được kiểu dữ liệu, thư viện \(readr\) sẽ lưu dưới dạng vec-tơ kiểu chuỗi ký tự. Để làm việc được trên dữ liệu, bạn đọc cần định dạng lại các cột cho đúng với mong muốn. Các hàm thuộc nhóm parse_*() trong thư viện \(readr\) hỗ trợ bạn đọc làm việc này. Nhóm hàm parse_*() có đầu vào là một véc-tơ kiểu chuỗi ký tự và đầu ra sẽ là kiểu dữ liệu mà bạn đọc mong muốn. Đối với mỗi kiểu dữ liệu, hàm parse_*() tương ứng sẽ có các tùy biến phù hợp. Trong phần tiếp theo của cuốn sách chúng tôi sẽ lần lượt giới thiệu các nhóm hàm parse_*() tương ứng với biến kiểu logic, biến kiểu số, biến kiểu thời gian và biến kiểu chuỗi ký tự.
5.2.2.1 Định dạng véc-tơ kiểu logic.
Định dạng lại một véc-tơ kiểu chuỗi ký tự thành kiểu logic là đơn giản nhất. Hàm số sử dụng trong trường hợp này là parse_logical(). Bạn đọc hãy quan sát ví dụ sau:
x<-c("TRUE","True","1","0","2",".","@","FALSE","false","f","F","T","t","true","false")
parse_logical(x, na = c(".", "@"))## [1] TRUE TRUE TRUE FALSE NA NA NA FALSE FALSE FALSE FALSE TRUE
## [13] TRUE TRUE FALSE
## attr(,"problems")
## # A tibble: 1 × 4
## row col expected actual
## <int> <int> <chr> <chr>
## 1 5 NA 1/0/T/F/TRUE/FALSE 2
Bạn đọc có thể thấy rằng tất cả các giá trị nằm trong véc-tơ \(x\) ở trên, ngoại trừ hai ký tự đặc biệt đã được khai báo trong hàm parse_logical() là \("."\) và \("@"\), thì chỉ có số 2 là không thể đổi sang biến kiểu logic. parse_logical() tự động đổi ký tự \("1"\) thành \(TRUE\) và ký tự \("0"\) thành \(FALSE\). Trong trường hợp véc-tơ \(x\) có kích thước lớn, các phần tử không thể đổi sang kiểu logic sẽ được lưu vào một \(tibble\). Bạn đọc sử dụng hàm problems() để lấy các các giá trị này.
x1<-sample(x, 10^4, replace = TRUE)
y<-parse_logical(x1)
problems(y)## # A tibble: 1,915 × 4
## row col expected actual
## <int> <int> <chr> <chr>
## 1 3 NA 1/0/T/F/TRUE/FALSE 2
## 2 13 NA 1/0/T/F/TRUE/FALSE 2
## 3 21 NA 1/0/T/F/TRUE/FALSE .
## 4 28 NA 1/0/T/F/TRUE/FALSE @
## 5 32 NA 1/0/T/F/TRUE/FALSE @
## 6 33 NA 1/0/T/F/TRUE/FALSE 2
## 7 34 NA 1/0/T/F/TRUE/FALSE .
## 8 39 NA 1/0/T/F/TRUE/FALSE .
## 9 47 NA 1/0/T/F/TRUE/FALSE 2
## 10 55 NA 1/0/T/F/TRUE/FALSE .
## # … with 1,905 more rows
Cột \(row\) cho biết vị trí của các phần tử trong véc-tơ \(x1\) không thể đổi sang kiểu logic. Giá trị của các phần tử này nằm trong cột \(actual\). Bạn đọc có thể quan sát các giá trị trong cột \(actual\) để tìm hiểu nguyên nhân tại sao parse_logical() không thể hoạt động trên các giá trị này.
5.2.2.2 Định dạng véc-tơ kiểu số.
Khi \(readr\) không thể tự định dạng một véc-tơ có kiểu số, các vấn đề thường gặp phải là:
Cách đánh số thập phân của các số trong véc-tơ. Tại Việt Nam số thập phân được sử dụng là dấu “,” trong khi R hiểu số thập phân là dấu “.”. Nhiều quốc gia khác trên thế giới cũng sử dụng dấu thập phân là dấu “,”.
Cách viết các số sử dụng cùng với các ký tự “.” hoặc “,” để người đọc dễ dàng đọc số đó. Chẳng hạn như tại Việt Nam, chúng ta viết số 1 tỷ như sau: 1.000.000.000; tại Thụy Sỹ số 1 tỷ được viết thành 1’000’000’000; chúng ta cần định dạng lại cho các giá trị kiểu như vậy để R hiểu được đây là các con số.
Khi các con số đi kèm theo đơn vị, chẳng hạn như đi kèm với ký hiệu tiền tệ: “100.000 đồng”, “100.000 vnd”, hoặc đi kèm với ký hiệu % như “50%”, chúng ta \(readr\) cũng sẽ không thể tự động chuyển đổi sang kiểu số.
Bạn đọc có thể sử dụng parse_double() hoặc parse_number() khi gặp phải các vấn đề ở trên. Chẳng hạn như khi gặp vấn đề về dấu “,” đối với dấu thập phân (với các số thập phân viết theo kiểu Việt Nam), bạn đọc sử dụng parse_number() với tùy biến locale = locale(decimal_mark = ",") để định dạng cho véc-tơ kiểu ký tự:
x<-c("0,5","1,5") # véc-tơ chứa các số 0,5 và 1,5; dấu thập phân là dấu ","
parse_number(x, locale = locale(decimal_mark = ","))## [1] 0.5 1.5
Khi gặp phải vấn đề về thứ hai, chúng ta sử dụng tùy biến \(grouping\_mark\) trong hàm \(locate()\). Trong ví dụ dưới đây sử dụng đồng thời hai tùy biến \(decimal\_mark\) và \(grouping\_mark\) của hàm locate()
x<-c("1.000,5","1.000.000,5") # véc-tơ chứa các số 1000,5 và 1000000,5; dấu thập phân là dấu ","
parse_number(x, locale = locale(decimal_mark = ",",
grouping_mark = "."))## [1] 1000.5 1000000.5
Khi gặp phải chuỗi ký tự chứa biến kiểu số đi kèm với đơn vị tiền tệ, hoặc dấu “%”, hàm \(parse_number()\) vẫn rất hiệu quả trong việc đổi chuỗi ký tự về kiểu số
x<-c("1.000,5 đồng","1.000.000,5 vnd")
parse_number(x, locale = locale(decimal_mark = ",",
grouping_mark = "."))## [1] 1000.5 1000000.5
5.2.2.3 Định dạng véc-tơ kiểu thời gian
Hàm số parse_datetime() có thể sử dụng để chuyển đổi các véc-tơ kiểu chuỗi ký tự sang véc-tơ kiểu thời gian và véc-tơ kiểu ngày tháng.
x<-c("1/2/2023", "23/10/2023 ", "01/01/1900")
parse_datetime(x, format = "%d/%m/%Y",
na = c("01/01/1900"))## [1] "2023-02-01 UTC" "2023-10-23 UTC" NA
Hai tùy biến của hàm parse_datetime() mà bạn đọc cần lưu ý là \(na\) và \(format\). Tùy biến \(na\) như đã sử dụng ở phần trước là một véc-tơ chứa các giá trị mà bạn đọc cho rằng đây là các giá trị không quan sát được. Trong véc-tơ \(x\) ở trên, giá trị “01#01#1990” nếu không có trong tùy biến \(na\) sẽ có kết quả là một ngày tháng có ý nghĩa trong véc-tơ kết quả. Tuy nhiên, bằng một cách nào đó, nếu bạn đọc biết rằng giá trị này do người nhập liệu đưa vào do không quan sát được biến đó, việc chuyển đổi biến thành giá trị ngày tháng sẽ làm sai lệch phân tích. Do đó bạn đọc cần khai báo giá trị này vào trong véc-tơ \(na\).
Tùy biến \(format\) trong hàm parse_datetime() là để bạn đọc gợi ý cho R định dạng của biến kiểu ngày tháng. Khi gán giá trị cho \(format\) bạn đọc cần lưu ý
Mỗi thành phần của biến thời gian (ngày, tháng, năm, giờ, phút, giây,…) được định nghĩa bắt đầu bằng \("%"\) và theo sau 1 chữ cái, chẳng hạn như bạn đọc sử dụng “%Y” khi muốn nói với R rằng biến kiểu thời gian nằm trong chuỗi ký tự được sử dụng 4 chữ số để chỉ định.
Các ký tự không liên quan đến các thành phần của thời gian, ngoại trừ các khoảng trắng phía trước và sau biến thời gian, cần phải được khai báo chính xác.
x<-c(" 1@2@2023-23#25#01 ", " 23@10@2023-01#06#59 ", "01@01@2023-00:00:00")
parse_datetime(x, format = "%d@%m@%Y-%H#%M#%S")## [1] "2023-02-01 23:25:01 UTC" "2023-10-23 01:06:59 UTC"
## [3] NA
Từ ví dụ trên bạn đọc có thể thấy rằng
Cần khai báo chính xác các ký tự nằm giữa các biến thời gian. Ký tự \("@"\) nằm giữa các giá trị ngày, tháng, năm; phân tách giữa ngày tháng với thời gian trong ngày là ký tự \("-"\); phân tách giữa các thành phần của thời gian trong ngày là ký tự \("#"\). Tất cả đều cần phải được khai báo chính xác với tùy biến \(format\). Giá trị thứ ba trong véc-tơ \(x\) gặp vấn đề vì phân tách giữa các thành phần của thời gian trong ngày sử dụng dấu “:”
Các khoảng trắng nằm trước và sau các chuỗi ký tự được bỏ qua và không ảnh hưởng đến kết quả.
Để biết chính xác cách gán giá trị cho tùy biến \(format\), bạn đọc nên tham khảo hướng dẫn sử dụng hàm parse_datetime(). Chúng tôi tóm tắt cách định dạng các thành phần của một biến thời gian trong bảng dưới đây:
| Thành phần | Định dạng chi tiết |
|---|---|
| Năm | %Y (4 chữ số) và %y (1 đến 2 chữ số) |
| Tháng | %m (1-2 chữ số), %b (tên tháng viết tắt), %B (tên tháng đầy đủ) |
| Ngày | %d (1-2 chữ số) |
| Giờ | %H (1-2 chữ số) |
| Phút | %M (1-2 chữ số) |
| Giây | %S (1-2 chữ số) |
Lưu ý rằng khi bạn đọc sử dụng %y, các ký tự “00” đến “69” sẽ được chuyển thành năm 2000 đến năm 2069 trong khi các ký tự từ “70” đến 99 sẽ được chuyển thành năm 1970 đến 1999. Ngoài ra, thành phần tháng của biến thời gian trong nhiều dữ liệu thường được viết dưới dạng chuỗi ký tự thay vì sử dụng số. Do đó bạn đọc cần các gợi ý %b hoặc %B để R có thể hiểu được:
x<-c("sep 21, 23 ", " JAN 1, 69 ", "Dec 25, 70")
parse_datetime(x, format = "%b %d, %y")## [1] "2023-09-21 UTC" "1969-01-01 UTC" "1970-12-25 UTC"
5.2.3 Định dạng cột kiểu chuỗi ký tự
Khi bạn đọc dùng \(readr\) để đọc dữ liệu từ nguồn ngoài vào R, cột dữ liệu không rõ định dạng sẽ được lưu dưới dạng véc-tơ chuỗi ký tự. Vậy tại sao cần định dạng lại thành véc-tơ kiểu chuỗi ký tự? Nghe có vẻ vô lý nhưng đây lại là vấn đề phức tạp nhất trong định dạng lại cột dữ liệu. Để hiểu vấn đề này bạn đọc cần tìm hiểu một chút về cách máy tính điện tử lưu và mở một chuỗi ký tự. Giả sử bạn đọc muốn gửi một dữ liệu chứa ký tự “a” đến một máy tính khác. Sau khi viết ký tự “a” lên một phần mềm soạn thảo văn bản nào, bạn đọc sẽ cần lưu ký tự “a” lên máy tính của bạn. Tất nhiên máy tính của bạn sẽ không thể ghi nhớ chữ “a” một cách tượng hình mà sẽ mã hóa (hay thuật ngữ chuyên ngành gọi là \(encode\)) chữ “a” thành một đoạn mã nhị phân bao gồm 0 và 1 mà máy tính có thể lưu được. Khi bạn gửi dữ liệu sang một máy tính khác, đoạn mã bao gồm 0 và 1 đó sẽ được gửi đi. Khi máy tính điện tử khác mở dữ liệu, đoạn mã nhị phân sẽ được giải mã (thuật ngữ chuyên ngành gọi là \(decode\)) để hiển thị. Sẽ không có vấn đề gì xảy ra nếu quy tắc mã hóa và giải mã được thống nhất và chữ “a” sẽ được hiển thị chính xác trên máy tính thứ hai.
Thực tế là trước khi có bộ mã hóa và quy tắc mã hóa chung được công nhận rộng rãi như Unicode và UTF-8, rất khó để có sự thống nhất quy tắc mã hóa ký tự. May mắn là đến thời điểm chúng tôi viết cuốn sách này đa số các hệ điều hành, hệ soạn thảo văn bản,… đều sử dụng bảng mã Unicode và bộ mã hóa UTF-8. Giải thích chi tiết về bộ mã hóa hay quy tắc mã hóa là rất phức tạp và vượt quá nội dung của cuốn sách này. Chúng tôi chỉ cần bạn đọc hiểu về Unicode và UTF-8 như sau:
Unicode là một bảng mã chuẩn được công nhận rộng rãi cho biết quy tắc cho tương ứng hầu hết các ký tự từ đơn giản đến phức tạp, kể cả các ngôn ngữ sử dụng ký tự tượng hình phức tạp như chữ Hán của tiếng Trung Quốc, tiếng Nhật, chữ Nôm của tiếng Việt, với một số nằm giữa số 0 đến số \(10FFFF\) khi viết theo hệ 16. Một số khi viết trong hệ 16 có thể sử dụng (0, 1, …, 9, A, B, C, D, E, F) để biểu diễn, do đó số các ký tự mà bảng mã Unicode có thể đưa vào là \(16^4 + 16^5 = 1.114.112\) ký tự, bao gồm \(16^5\) số từ 0 đến FFFFF và \(16^4\) số từ 100000 đến 10FFFF.
UTF-8 là quy tắc lưu các số viết trong hệ 16 của bảng mã Unicode thành các chuỗi nhị phân 0 và 1 mà máy tính có thể phân biệt được. Số 8 ở đây có nghĩa là 8 bit hay một byte là 8 giá trị 0 và 1 đứng liền nhau. Một ký tự bất kỳ trong bảng mã Unicode đều có thể được mã hóa thành 1, 2, 3 hoặc nhiều byte theo quy tắc mã hóa UTF-8.
Quay trở lại vấn đề định dạng lại dữ liệu kiểu chuỗi ký tự, sẽ không có vấn đề xảy ra nếu người nhập liệu sử dụng bộ mã hóa UTF-8 bởi \(readr\) luôn sử dụng UTF-8 để giải mã. Trong thực tế thì vẫn còn một số hệ thống, hoặc hệ soạn thảo văn bản sử dụng cách mã hóa không tương thích với UTF-8. Giả sử khi đọc một dữ liệu từ nguồn ngoài vào bằng read_csv() và cho kết quả như sau
x<-read_csv("../KHDL_KTKD/Dataset/Book1.csv")
x## # A tibble: 5 × 2
## A B
## <chr> <chr>
## 1 "l\xea" 20.000 vnd
## 2 "t\xe1o" 35.000 vnd
## 3 "qu\xfdt" 30.000 vnd
## 4 "c\xe0 t\xedm" 5.500 vnd
## 5 "m\xedt" 10.000 vnd
Cột \(A\) của dữ liệu đã không được lưu bằng mã hóa UTF-8 nên thư viện \(readr\) không hiển thị được các giá trị có ý nghĩa. Để định dạng lại cột dữ liệu, bạn đọc sử dụng hàm parse_character() với tùy biến \(encoding\). Không dễ để biết được dữ liệu đã được mã hóa bằng bộ mã hóa nào. Thư viện \(readr\) cung cấp hàm guess_encoding() hỗ trợ bạn đọc dự đoán một biến kiểu chuỗi ký tự đã được mã hóa bẳng bộ mã hóa nào. Tuy nhiên trải nghiệm của chúng tôi với hàm số này là không tốt. Lời khuyên của chúng tôi là bạn đọc nếu có thể hãy tìm hiểu nguồn gốc của dữ liệu: dữ liệu được sính ra từ đâu, hệ thống nào,… Trong trường hợp việc này là không thê, bạn đọc hãy thử giải mã đoạn văn bản bằng một số bộ mã hóa thường gặp cho đến khi gặp được kết quả mong muốn! Trong trường hợp dữ liệu ở trên do nguồn là tiếng Việt nên chúng ta có thể thử các
parse_character(x$A, locale = locale(encoding = "Latin2"))## [1] "lę" "táo" "quýt" "cŕ tím" "mít"
Kết quả khi sử dụng bộ mã \(Latin2\) đã cho một vài giá trị có ý nghĩa, chúng ta tiếp tục thử với \(Latin1\)
parse_character(x$A, locale = locale(encoding = "Latin1"))## [1] "lê" "táo" "quýt" "cà tím" "mít"
May mắn là cột dữ liệu đều đã có thể đọc được với chúng ta. Đối với cột \(B\) của dữ liệu bạn đọc có thể sử dụng parse_numbder() như đã trình bày ở trên. Dữ liệu sau khi được định dạng lại đã dễ hiểu hơn rất nhiều
tibble(Name = parse_character(x$A, locale = locale(encoding = "Latin1")),
Price = parse_number(x$B, locale = locale(grouping_mark = ".")))## # A tibble: 5 × 2
## Name Price
## <chr> <dbl>
## 1 lê 20000
## 2 táo 35000
## 3 quýt 30000
## 4 cà tím 5500
## 5 mít 10000
5.3 Giá trị ngoại lai và giá trị không quan sát được.
Giá trị không quan sát được là các giá trị xuất hiện dưới dạng \(NA\) trong dữ liệu sau khi nhập vào R. Có nhiều lý do khác nhau dẫn đến việc dữ liệu không quan sát được, chẳng hạn như thông tin do người làm dữ liệu cung cấp không đầy đủ, những người cung cấp dữ liệu từ chối chia sẻ thông tin, hệ thống quản lý dữ liệu bị lỗi, hoặc cũng có thể do người quản lý xóa dữ liệu vì lý do bảo mật. Giá trị không quan sát được ngoài các giá trị \(NA\) xuất hiện trong dữ liệu, mà còn là các giá trị không phù hợp với kiểu dữ liệu hoặc miền giá trị của cột dữ liệu. Đối với một vài hệ thống khi dữ liệu được xuất ra giá trị không quan sát được vẫn được ghi nhận bằng một giá trị nào đó. Bạn đọc cần cẩn trọng khi làm việc với những dữ liệu kiểu như vậy.
Giá trị ngoại lai hay còn được gọi là giá trị bất thường là một điểm dữ liệu hoặc một quan sát sai khác đáng kể so với đa số các quan sát khác. Một giá trị ngoại lai xuất hiện trong dữ liệu có thể là do lỗi trong quản lý dữ liệu, do sai số trong đo lường hoặc cũng có thể do bản chất phân phối của dữ liệu. Tùy theo nguồn gôc của giá trị ngoại lai mà chúng ta có cách xử lý dữ liệu khác nhau.
Khi không được xử lý thích hợp, giá trị không quan sát được và các giá trị ngoại lai có thể làm sai lệch kết luận của tất cả các phân tích về dữ liệu, khiến người quản lý đưa ra quyết định sai lầm. Bạn đọc quan sát ví dụ sau:
## # A tibble: 4 × 6
## MSV Name Age Gender `Height (cm)` `Weight (kg)`
## <chr> <chr> <chr> <chr> <dbl> <dbl>
## 1 MSV00001 12345 30 Nam 1.76 68
## 2 MSV43241 Nguyễn Văn An Nhập sai ngày sinh N 169 72
## 3 MSV65432 Lê Thị Loan -1 Nữ 155 48
## 4 MSV34 Trần Mạnh Cường 15 <NA> 175 150
Trong dữ liệu ở trên, mặc dù chỉ có 1 giá trị đang là \(NA\) ở cột giới tính nhưng nếu quan sát kỹ bạn đọc sẽ nhận ra rằng:
Cột \(name\): giá trị “12345” không thể là tên của một sinh viên, do đó đây cũng là một giá trị không quan sát được.
Cột \(Age\): thứ nhất, giá trị ở hàng thứ hai của cột \(Age\) là kiểu chuỗi ký tự. Thứ hai, tuổi của một sinh viên không thể là số âm, nên giá trị \(-1\) không phù hợp với miền giá trị của cột này. Cột \(Age\) có hai giá trị không quan sát được.
Cột \(Gender\): có giá trị là ký tự \(N\) không rõ là thể hiện cho giới tính Nam hay Nữ, giá trị này cũng là không quan sát được.
Cột \(MSV\): Giả sử bằng một cách nào đó, bạn đọc biết rằng mã sinh viên phải là một đoạn ký tự có độ dài là 8, bao gồm đoạn ký tự “MSV” và theo sau là 5 chữ số, thì giá trị “MS34” cũng là một giá trị không quan sát được ở cột mã sinh viên.
Để xác định dữ liệu có giá trị ngoại lai hay không cần sử dụng các kiến thức về thống kê toán:
Cột \(Height\) có giá trị chiều cao ở hàng thứ nhất là 1,76 cm. Giá trị này quá nhỏ để làm chiều cao của một người bình thường. Nhiều khả năng khi đo chiều cao của sinh viên, người nhập dữ liệu đã ghi lại theo đơn vị mét.
Cột \(Weight\) có giá trị cân nặng của hàng thứ 4 là 150 kg. Mặc dù dữ liệu có rất ít quan sát để đưa ra kết luận phân phối xác suất của cân nặng của sinh viên là gì, tuy nhiên với kiến thức thực tế chúng ta có thể kết luận rằng 150 kg là một cân nặng lớn bất thường với các giá trị cân nặng còn lại. Đây nhiều khả năng là một giá trị ngoại lai.
Để xác định các giá trị không quan sát được và giá trị ngoại lai tùy thuộc vào từng dữ liệu cụ thể và kiến thức tổng hợp và kiến thức chuyên môn của người xử lý dữ liệu và nằm ngoài phạm vi thảo luận của cuốn sách này. Dữ liệu ở trên chỉ là một dữ liệu nhỏ và đơn giản nên việc xác định các giá trị không quan sát được và giá trị ngoại lai là không khó. Chúng ta biến đổi các giá trị không quan sát được thành \(NA\) như sau
df$MSV[(nchar(df$MSV)!=8)]<-NA # mã sinh viên không có 8 ký tự là không quan sát được
df$Name[df$Name=="12345"]<-NA
df$Age<-parse_number(df$Age, na = c("-1")) # tuổi có giá trị (-1) là không quan sát được
df$Gender[df$Gender == "N"]<-NAĐối với các giá trị ngoại lai, chúng ta sẽ đổi giá trị bị ghi nhận sai đơn vị về đúng đơn vị. Với giá trị cân nặng 150 kg, do dữ liệu nhỏ, bạn đọc có thể giữ nguyên giá trị này hoặc thay thế giá trị này bằng giá trị lớn nhất của những người có cân nặng thông thường.
df$Height[1]<-df$Height[1] * 100 # đổi đơn vị đo từ mét sang cmDữ liệu sau khi xử lý giá trị ngoại lai và định nghĩa lại các giá trị không quan sát được như sau:
df## # A tibble: 4 × 6
## MSV Name Age Gender `Height (cm)` `Weight (kg)`
## <chr> <chr> <dbl> <chr> <dbl> <dbl>
## 1 MSV00001 <NA> 30 Nam 1.76 68
## 2 MSV43241 Nguyễn Văn An NA <NA> 169 72
## 3 MSV65432 Lê Thị Loan NA Nữ 155 48
## 4 <NA> Trần Mạnh Cường 15 <NA> 175 150
Hàm số is.na(df) trả lại giá trị là \(TRUE\) nếu dữ liệu là quan sát là \(NA\) và trả lại giá trị \(FALSE\) nếu không phải \(NA\). Bạn đọc có thể dùng hàm is.na() kết hợp với hàm sum() để tính toán mỗi cột có bao nhiêu giá trị không quan sát được và tỷ lệ số giá trị không quan sát được trên mỗi cột là bao nhiêu:
## MSV Name Age Gender Height (cm) Weight (kg)
## 1 1 2 2 0 0
## MSV Name Age Gender Height (cm) Weight (kg)
## 0.25 0.25 0.50 0.50 0.00 0.00
Với những dữ liệu nhỏ thì hiển thị trực tiếp số lượng giá trị \(NA\) trong mỗi cột là hiệu quả nhất. Khi dữ liệu có nhiều quan sát và nhiều biến, bạn đọc nên sử dụng đồ thị để mô tả số lượng hoặc tỷ lệ giá trị không quan sát được của mỗi cột. Ví dụ như với dữ liệu \(gapminder\) của thư viện \(dslabs\), sử dụng đồ thị \(barplot\) để mô tả giá trị không quan sát được sẽ hiệu quả hơn:
y<-sapply(gapminder,function(x) sum(is.na(x))/length(x)*100) # cho biết tỷ lệ giá trị NA trong mỗi cột
barplot(sort(y), main = "Tỷ lệ giá trị không quan sát được",
ylab = "Đơn vị %",
xlab = "",
col = "lightskyblue")
Xử lý giá trị không quan sát được dựa trên kinh nghiệm và hiểu biết về dữ liệu của bạn đọc luôn là ưu tiên trước tiên. Nếu chúng ta không có kinh nghiệm và hiểu biết về dữ liệu, các kỹ thuật xử lý dựa trên các nguyên tắc của xác suất thống kê sẽ được sử dụng.
5.3.1 Giá trị ngoại lai.
Giá trị ngoại lai, hay còn gọi là giá trị bất thường, là những giá trị mà khác xa tập hợp các giá trị còn lại. Không có một định nghĩa định lượng chính xác nào cho khái niệm như thế nào là khác xa các giá trị còn lại. Do đó, tùy theo bản chất của dữ liệu và tùy theo quan điểm của người phân tích dữ liệu mà một hay một số giá trị có khả năng là giá trị ngoại lai hay không. Giá trị ngoại lai thường chỉ được nhắc đến với các dữ liệu có số quan sát đủ lớn để đưa ra kết luận có ý nghĩa thống kê.

Khi dữ liệu có 10 quan sát như hình bên trái, có 8 quan sát màu xanh da trời nằm gần nhau hơn, điểm màu cam hơi xa hơn tập hợp các điểm màu xanh da trời một chút, còn điểm màu đỏ nằm xa hơn. Khi gặp dữ liệu như vậy, chúng ta có thể kết luận điểm màu đỏ là giá trị ngoại lai, còn kết luận điểm màu cam có phải ngoại lai hay không thì còn tùy thuộc vào ý tưởng của người phân tích dữ liệu. Chuyển sang hình bên phải với dữ liệu có 100 quan sát. Các điểm màu xanh da trời định hình khá rõ miền giá trị của trung tâm của dữ liệu là nằm xung quanh điểm (3,3). Chúng ta có thể kết luận một cách khá chắc chắn rằng điểm màu đỏ là một giá trị ngoại lai. Điểm màu cam, mặc dù nằm khá xa trung tâm của dữ liệu, nhưng để kết luận rằng có phải giá trị ngoại lai hay không vẫn phụ thuộc vào ý tưởng của người phân tích.
Nguồn gốc của giá trị ngoại lai là có thể có nhiều nguyên nhân khác nhau, bao gồm cả nguyên nhân khách quan hoặc nguyên nhân chủ quan. Các nguyên nhân khách quan có thể do nguồn sinh dữ liệu, hay hệ thống quản lý dữ liệu gặp sự cố, do lỗi trong quá trình truyền hoặc sao chép dữ liệu. Nguyên nhân chủ quan bao gồm có các hành vi gian lận, lỗi nhập và sao chép dữ liệu của con người, hoặc các giá trị được cố tình đưa vào trong dữ liệu với mục đích lấy phản hồi từ người dùng dữ liệu.
Nếu không xử lý giá trị ngoại lai kết quả tính toán sẽ bị sai lệch đáng kể. Dữ liệu có kích thước càng nhỏ thì ảnh hưởng của giá trị ngoại lai lại càng lớn. Trong ví dụ ở trên, giả sử bạn đọc cần phân tích sự tác động của biến \(X\) (trục nằm ngang) lên biến \(Y\) (giá trị trên trục dọc) bằng một mối quan hệ tuyến tính. Hãy quan sát kết quả của phân tích khi chúng ta
Giữ nguyên 10 quan sát và phân tích mối liên hệ tuyến tính.
Loại bỏ điểm A (màu đỏ) ra và phân tích mối liên hệ tuyến tính.
Loại bỏ điểm A (màu đỏ) và điểm B (màu cam) ra để phân tích mối liên hệ tuyến tính.

Khi giữ nguyên 10 điểm dữ liệu để xây dựng mối quan hệ tuyến tính, đường thẳng mô tả mối quan hệ giữa \(Y\) và \(X\) là nằm trong hình phía bên trái. Đường thẳng này có hệ số góc dương (một đường dốc lên khi đi từ trái sang phải), điều này có nghĩa là biến \(X\) có tác động cùng chiều lên biến \(Y\). Sau khi loại bỏ điểm A (màu đỏ) và tính toán lại, đường thẳng mô tả mối quan hệ tuyến tính giữa \(Y\) và \(X\) ở là đường thẳng trong hình ở giữa. Đường thẳng gần như nằm ngang, cho thấy \(X\) ít có tác động lên biến \(Y\). Sau cùng, trong hình phía bên phải, sau khi loại bỏ các điểm A (màu đỏ) và điểm B (màu cam), đường thẳng mô tả mối quan hệ tuyển tính giữa \(Y\) và \(X\) là đường dốc xuống, nghĩa là mối tác động của \(X\) lên \(Y\) là ngược chiều. Bạn đọc có thể thấy rằng kết luận đưa ra sau khi phân tích thay đổi hoàn toàn khi chúng ta có các lựa chọn khác nhau về loại bỏ các giá trị được cho là ngoại lai ra khỏi dữ liệu. Sự tác động của \(X\) lên \(Y\) từ thuận chiều (hình bên trái) chuyển sang không có mối liên hệ (hình ở giữa) và sau cùng là sự tác động ngược chiều của \(X\) lên \(Y\) (hình bên phải).
Trong phần tiếp theo chúng thôi sẽ thảo luận về các phương pháp dùng để xác định các giá trị ngoại lai trong dữ liệu.
5.3.2 Cách phát hiện giá trị ngoại lai
Không có một định nghĩa chính xác như thế nào là giá trị ngoại lai, chính vì thế không có phương pháp tổng thể và thống nhất nào để phát hiện giá trị ngoại lai trong dữ liệu. Với mỗi cách nhìn nhận giá trị ngoại lại khác nhau mà có phương pháp tiếp cận cụ thể để xác định các giá trị đó. Chúng tôi chỉ trình bày các phương pháp chung được chấp nhận rộng rãi bởi những người phân tích dữ liệu. Các phương pháp đơn giản sẽ được trình bày cụ thể ngay trong phần này. Các phương pháp phức tạp hơn đòi hỏi kiến thức của các chương sau của cuốn sách sẽ được trình bày dưới dạng ý tưởng và hàm có sẵn trong thư viện bổ sung.
5.3.2.1 Phát hiện giá trị ngoại lai trong một véc-tơ.
Để xác định một giá trị là giá trị ngoại lai hay không luôn bao gồm hai bước, bước thứ nhất là sử dụng các phương pháp xác suất thống kê để xác định các giá trị có nhiều khả năng là ngoại lai, và bước thứ hai là sử dụng kiến thức chuyên môn hoặc hỏi ý kiến chuyên gia (nếu có thể) để khẳng định lại kết quả từ bước thứ nhất.
Nếu véc-tơ là một véc-tơ kiểu chuỗi ký tự (không phải kiểu factor) thì không có quy tắc rõ ràng nào để xác định giá trị ngoại lai. Một chuỗi ký tự có thể là ngoại lai nếu chuỗi ký tự có độ dài bất thường, có chứa nhiều ký tự bất thường, một chuỗi ký tự không có ý nghĩa, hoặc cũng có thể là một chuỗi ký tự trống,… việc này hoàn toàn phụ thuộc vào cách tiếp cận của người phân tích dữ liệu. Các phương pháp xử lý dữ liệu kiểu chuỗi ký tự hiện đại có khả năng biến đổi một chuỗi ký tự thành một véc-tơ kiểu số. Việc xác định chuỗi ký tự có phải là một giá trị bất thường hay không sẽ liên quan đến việc xác định một véc-tơ kiểu số có phải là một véc-tơ có giá trị bất thường trong một tập hợp các véc-tơ. Các kỹ thuật này vượt quá phạm vi của cuốn sách
Đối với véc-tơ kiểu factor hay véc-tơ kiểu logic, giá trị có khả năng là ngoại lai là các giá trị xuất hiện với tần xuất rất nhỏ. Chẳng hạn như khi mô tả một véc-tơ chứa tên các loại đồ uống được bán trong một siêu thị trong tháng vừa rồi, bạn gặp trường hợp sau:
x<-sample(c("Coca","Pepsi","Red bull","Mirinda","Collagen"),10000,prob = c(4000,3000,2000,2000,5),replace = TRUE)
barplot(sort(table(x)/length(x)),col = "lightskyblue")
Khi gặp đồ thị như trên, có khả năng đồ uống có tên “Collagen” là giá trị ngoại lai. Nếu siêu thị có bán loại đồ uống này và việc sản phẩm không được khách hàng ưa chuộng, việc xuất hiện với tần xuất thấp là bình thướng và đây không phải là giá trị ngoại lai. Tuy nhiên việc tên sản phẩm xuất hiện trong danh sách bán hàng tháng này dù siêu thị không bán cũng có thể là do lỗi gặp phải trong quản lý hệ thống bán hàng khi đã ghi nhận tên “Collagen” cho một đồ uống khác.
Đối với véc-tơ kiểu số, các giá trị có khả năng là ngoại lai thường là các giá trị nằm ở đuôi của phân phối xác suất. Để biết một véc-tơ kiểu số có giá trị ngoại lai hay không, bạn đọc nên sử dụng đồ thị boxplot. Các điểm nằm phía dưới điểm nhỏ nhất (Q0) và nằm phía trên điểm lớn nhất (Q4) của đồ thị boxplot có nhiều khả năng là các giá trị ngoại lai. Điểm nhỏ nhất và điểm lớn nhất của đồ thị boxplot được xác định dựa trên mức từ phân vị thứ nhất (Q1) và mức tứ phân vị thứ 3 (Q3): \[\begin{align} &&\text{Inter Quartile Range (IQR)} = Q3 - Q1 \\ &&\text{Điểm nhỏ nhất (Q0)} = Q1 - 1.5 \times IQR \\ &&\text{Điểm lớn nhất (Q4)} = Q3 + 1.5 \times IQR \end{align}\]
Các giá trị nằm ngoài khoảng \((Q1 - 1,5 \times IQR, Q3 + 1.5 \times IQR)\) có nhiều khả năng là giá trị ngoại lai. Giá trị càng thấp hơn Q0 và càng cao hơn Q4 thì khả năng là giá trị ngoại lai lại càng cao.
Đồ thị boxplot dưới đây mô tả phân phối của véc-tơ chứa khối lượng giao dịch, tính bằng triệu cổ phiếu/ngày, của cổ phiếu của tập đoàn FLC. Cổ phiếu được niêm yết trên sàn giao dich chứng khoán Thành phố Hồ Chí Minh từ ngày 6 tháng 10 năm 2011 đến ngày 8 tháng 9 năm 2022. Dữ liệu có hơn quan sát.
dat1<-read_csv("../KHDL_KTKD/Dataset/FLC.csv")
dat1%>%filter(year(Date)>=2021)%>%ggplot(aes(y=Volume/10^6))+
geom_boxplot(fill = "lightskyblue",alpha=0.5)+
theme_minimal()+
labs(title = "Khối lượng giao dịch cổ phiếu FLC",
subtitle = "Đơn vị: Triệu cổ phiếu/ngày",
caption = "Nguồn dữ liệu: Sở giao dịch chứng khoán TP HCM")+
theme(legend.position="none")+theme(axis.text=element_text(size=12),
axis.title=element_text(size=12,face="bold"))+
ylab("")+xlab("")+
theme(plot.title = element_text(size = 14, face = "bold"))
Chúng ta có thể thấy trên đồ thị boxplot không có điểm nằm dưới Q0. Có 8 quan sát có giá trị lớn hơn Q4; các giá trị này có khả năng là các giá trị ngoại lai. Có 3 quan sát với giá trị lớn hơn 100, nghĩa là có ba ngày mà có hơn 100 triệu cổ phiếu FLC được giao dịch. Nếu có một chút kinh nghiệm về thị trường chứng khoán Việt Nam, bạn đọc có thể kiểm chứng được đây là số lượng cổ phiếu giao dịch lớn bất thường.
Thực tế thì ba phiên giao dịch có khối lượng giao dịch lớn hơn 100 triệu cổ phiếu là các phiên giao dịch ngày 10 tháng 1 năm 2022, ngày 11 tháng 1 năm 2022 và phiên giao dịch ngày 1 tháng 4 năm 2022. Thực tế cho thấy đây là ba phiên giao dịch mà cổ phiếu FLC đã bị thao túng giá và dẫn đến việc cố phiếu FLC bị cấm giao dịch trên sàn giao dịch TP HCM kể từ tháng 09 năm 2022.
Sau một vài tháng giá cổ phiếu FCL tăng lên gấp 2 lần, đến ngày 10 và ngày 11 tháng 01 năm 2022, các cổ đông chính của FLC bán ra khối lượng rất lớn các cổ phiếu mà không đăng ký với Ủy ban chứng khoán theo quy định. Sau hai phiên giao dịch này giá cổ phiếu FLC giảm mạnh về đến mức trước đó vài tháng.
Ngày 31 tháng 03 năm 2022 các thông tin giả mạo về nhu cầu mua cổ phiếu FLC với khối lượng lớn được đưa ra (sau nhiều ngày giá cổ phiếu FLC giảm hết biên độ) làm cho nhu cầu mua FLC trong ngày 01 tháng 04 năm 2022 cao đột biến.
Đây là ví dụ điển hình về dữ liệu có giá trị ngoại lai có nguyên nhân chủ quan từ con người. Bạn đọc có thể sử dụng kết hợp đồ thị boxplot và các đồ thị mô tả phân phối của biến liên tục như đồ thị \(histogram\) hay đồ thị \(density\). Hình vẽ dưới đây mô tả phân phối của chiều cao của 245 nam giới là nhân viên của một công ty. Đơn vị đo chiều cao là \(cm\).

Đồ thị boxplot và histogram đều cho thấy trong dữ liệu có các giá trị chiều cao của nam giới xấp xỉ giá trị 0 và nhiều khả năng đây là các giá trị ngoại lai. Đồ thị histogram còn cho thấy có nhiều hơn 1 giá trị có giá trị như vậy. Lọc các giá trị đó ra khỏi véc-tơ chúng ta sẽ thu được 5 giá trị là 1,52; 1,74; 1,70; 1,62; và 1,80. Đây không thể là chiều cao của nam giới đo bằng đơn vị \(cm\). Có nhiều khả năng là khi ghi lại chiều cao của các nhân viên này, người nhập dữ liệu đã sử dụng đơn vị là \(mét\) thay vì \(cm\). Chúng ta có thể sửa các giá trị ngoại lai này bằng cách đổi từ đơn vị \(mét\) sang \(cm\). Phân phối của chiều cao sau khi sửa lại dữ liệu được mô tả như hình dưới đây:

Véc-tơ kiểu số có mô tả giá trị đo lường, tiền tệ rất thường xuyên gặp vấn đề như kể trên. Ngay khi gặp giá trị ngoại lai trong véc-tơ kiểu số như trên bạn đọc hãy nghĩ đến sai đơn vị đo lường là nguyên nhân đầu tiên.
Ngoài việc sử dụng các tứ phân vị để phát hiện giá trị ngoại lai, một phương pháp định lượng khác cũng thường được đề cập đến trong nhiều tài liệu là sử dụng \(Z-Score\). \(Z-Score\) được tính bằng khoảng cách từ 1 điểm đến giá trị trung bình của dữ liệu sau đó chia cho độ lệch chuẩn của dữ liệu \[\begin{align} Z-Score(x_i) = \cfrac{|x_i - \bar{x}|}{\sigma(x)} \end{align}\]
\(Z-Score\) dựa trên giả thiết là dữ liệu có phân phối chuẩn, do đó các điểm dữ liệu có \(Z-Score\) lớn, thường sử dũng ngưỡng lớn hơn 3, được coi là các giá trị ngoại lai. Chẳng hạn như khi vẽ \(Z-Score\) của tất cả các điểm dữ liệu trong dữ liệu về chiều cao cùa nhân viên, chúng ta sẽ có đồ thị như sau

Các điểm màu đỏ là các điểm bị ghi nhận sai đơn vị đo lường từ \(cm\) sang \(mét\) và có \(Z-Score\) lên đến hơn 6. Trong trường hợp này \(Z-Score\) cũng là phương pháp định lượng hiệu quả để xác định giá trị ngoại lai. Tuy nhiên, \(Z-Score\) có điểm bất lợi là giá trị này được tính toán dựa trên giá trị trung bình và độ lệch tiêu chuẩn của dữ liệu trong khi chính các giá trị đó lại phụ thuộc rất lớn vào các giá trị ngoại lai. Một cách đề giảm thiểu tác động của giá trị ngoại lai lên tính \(Z-Score\) là không tính đến \(x_i\) khi tính toán trung bình \(\bar{x}\) và \(\sigma(x)\).
Đa số các phương pháp xác định giá trị ngoại lai ở trên đều dựa trên giả thiết là véc-tơ dữ liệu có phân phối chuẩn. Dữ liệu về bồi thường bảo hiểm là một điển hình của dữ liệu không có phân phối chuẩn. Đồ thì dưới đây mô tả số liệu về tiền bồi thường bảo hiểm sức khỏe của hơn 1.000 khách hàng của một công ty bảo hiểm nhân thọ

Nhiều điểm dữ liệu bị xác định là ngoại lai trong trường hợp này mặc dù đây là dữ liệu chính xác. Trong thực tế, nếu bạn đọc gặp dữ liệu không có phân phối chuẩn, hãy biến đổi dữ liệu về gần với phân phối chuẩn nhất có thể trước khi thực hiện các bước phân tích. Phép biến đổi thường được sử dụng nhất là biến đổi Box-Cox.

Kỹ thuật biến đổi Box-Cox được trình bày trong phụ lục của chương này.
5.3.2.2 Giá trị ngoại lai trong không gian nhiều chiều
Xác định giá trị ngoại lai trong không gian nhiều chiều phức tạp hơn trong không gian một chiều rất nhiều. Trong không gian một chiều, chúng ta cần xác định những số nào là giá trị ngoại lai của một véc-tơ. Trong khi trong không gian nhiều chiều, chúng ta cần phải xác định các quan sát nào là giá trị ngoại lai trong một dữ liệu. Ngoài việc xem xét giá trị trong từng cột dữ liệu, chúng ta cần phải xem xét cả mối liên hệ giữa các véc-tơ (cột) đó.
Các phương pháp để xác định giá giá trị ngoại lai trong không gian nhiều chiều vẫn dựa trên nguyên tắc cơ bản áp dụng trong không gian một chiều, đó là các quan sát càng xa điểm trung tâm của dữ liệu thì quan sát đó càng có khả năng cao là giá trị ngoại lai. Khái niệm xa hay gần trong một không gian nhiều chiều luôn gắn liền với một khái niệm về khoảng cách. Khoảng cách thường sử dung nhiều nhất trong không gian nhiều chiều là khoảng cách Euclid. Tuy nhiên khoảng cách Euclid có nhược điểm là không tính đến mối liên hệ giữa các cột dữ liệu. Khoảng cách thường được dùng trong xác định giá trị ngoại lai là khoảng cách Mahalanobis.
Cho \(x_i = x_{i1}, x_{i2}, \cdots, x_{ip}\) là quan sát thứ \(i\) và \(\mu = \mu_{1}, \mu_{2}, \cdots, \mu_{p}\) là véc-tơ các giá trị trung bình của các véc-tơ cột. Khoảng cách Euclid và khoảng cách Mahalanobis được định nghĩa như sau \[\begin{align} D^{Euclid}(x_i,\mu) = \sqrt{(x_i - \mu)^T (x_i - \mu)} \\ D^{Mahalanobis}(x_i,\mu) = \sqrt{(x_i - \mu)^T \ \Sigma^{-1} \ (x_i - \mu)} \\ \end{align}\] trong đó \(D^{Euclid}(x_i,\mu)\) và \(D^{Mahalanobis}(x_i,\mu)\) lần lượt là khoảng cách Euclid và khoảng cách Mahalanobis từ quan sát \(x_i\) đến điểm trung bình \(\mu\). Trong công thức tính khoảng cách Mahalanobis, \(\Sigma^{-1}\) là ma trận nghịch đảo của ma trận hiệp phương sai của dữ liệu. Ki Có thể thấy rằng khoảng cách Euclid là trường hợp riêng của khoảng cách Mahalanobis khi các cột dữ liệu có phương sai bằng 1 và đôi một độc lập với nhau.
Bạn đọc có thể tự lập trình hàm số tính khoảng cách Euclid và hàm số tính khoảng cách Mahalanobis giữa 2 véc-tơ như sau
Dis.Euc<-function(x,y) sum((x-y)^2)^0.5
Dis.Mah<-function(x,y,Sigma) (t(x-y)%*% solve(Sigma) %*%(x-y))^0.5 Chúng ta quay trở lại ví dụ về dữ liệu bao gồm 10 quan sát với hai giá trị ngoại lai là điểm A (màu đỏ) và điểm B (màu cam). Chúng ta tính toán khoảng cách Euclid của mỗi điểm đến trung tâm của dữ liệu và sắp xếp các điểm theo thứ tự khoảng cách Euclid giảm dần
| Điểm dữ liệu | Tọa độ x | Tọa độ y | Khoảng cách Euclid | Khoảng cách Mahalanobis |
|---|---|---|---|---|
| Điểm A (đỏ) | 7.500 | 12.000 | 7.925 | 2.593 |
| Điểm B (cam) | 10.000 | 6.000 | 5.046 | 1.747 |
| Điểm khác (xanh) | 1.046 | 5.069 | 4.216 | 1.647 |
| Điểm khác (xanh) | 1.916 | 4.193 | 3.302 | 1.225 |
| Điểm khác (xanh) | 7.101 | 2.411 | 2.753 | 1.128 |
| Điểm khác (xanh) | 6.693 | 2.403 | 2.497 | 1.012 |
| Điểm khác (xanh) | 2.973 | 3.773 | 2.327 | 0.815 |
| Điểm khác (xanh) | 4.388 | 2.662 | 1.935 | 0.615 |
| Điểm khác (xanh) | 5.357 | 2.560 | 1.858 | 0.671 |
| Điểm khác (xanh) | 5.129 | 3.055 | 1.360 | 0.473 |
Có thế thấy rằng khi chỉ có 10 quan sát, khoảng cách Euclid có thể sử dụng để phát hiện được giá trị ngoại lai là điểm A và điểm B vì hai điểm này có khoảng cách đến trung tâm xa hơn so với các điểm còn lại. Khoảng cách Mahalanobis cũng cho kết quả tương tự. Tuy nhiên khoảng cách Euclid sẽ gặp vấn đề khi số lượng quan sát nhiều hơn và mối liên hệ giữa \(x\) và \(y\) rõ ràng hơn. Bạn đọc có thể thấy khoảng cách Euclid không cho kết quả tốt như khoảng cách Malahanobis trong trường hợp dữ liệu có 100 quan sát
| Điểm dữ liệu | Tọa độ x | Tọa độ y | Khoảng cách Euclid | Khoảng cách Mahalanobis |
|---|---|---|---|---|
| Điểm A (đỏ) | 7.500 | 12.000 | 9.501 | 8.010 |
| Điểm khác (xanh) | -1.993 | 6.897 | 7.100 | 2.441 |
| Điểm khác (xanh) | -2.448 | 5.970 | 7.072 | 2.430 |
| Điểm khác (xanh) | 10.216 | -0.163 | 7.011 | 2.377 |
| Điểm khác (xanh) | 9.862 | -0.211 | 6.725 | 2.290 |
| Điểm khác (xanh) | 10.061 | 0.291 | 6.667 | 2.269 |
| Điểm B (cam) | 10.000 | 6.000 | 6.606 | 4.675 |
| Điểm khác (xanh) | 10.022 | 0.591 | 6.508 | 2.240 |
| Điểm khác (xanh) | 8.732 | 0.131 | 5.581 | 1.932 |
| Điểm khác (xanh) | -0.787 | 5.044 | 5.183 | 1.810 |
Khi đo bằng khoảng cách Euclid, điểm \(A\) vẫn là điểm xa trung tâm dữ liệu nhất. Tuy nhiên khoảng cách từ điểm \(B\) đến trung tâm dữ liệu là nhỏ hơn một số điểm màu xanh khác. Quan sát khoảng cách Mahalanobis chúng ta có thể thấy rằng điểm \(A\) là điểm có khoảng cách xa nhất, sau đó đến điểm \(B\) (Malahanobis distance = 4.675), các điểm màu xanh khác đều có khoảng cách Mahalanobis nhỏ hơn 2,5.
Các kỹ thuật phát hiện giá trị ngoại lai phức tạp hơn dựa trên nguyên lý phân cụm sẽ được trình bày trong chương “học máy không có giám sát”. Nguyên tắc xác định một quan sát ngoại lai là phân chia dữ liệu thành các cụm sao cho các quan sát trong cùng một cụm có tính chất tương tự nhau. Các quan sát không nằm trong cụm nào, hoặc trong các cụm có rất ít quan sát, có nhiều khả năng là giá trị ngoại lai.
5.3.3 Xử lý giá trị ngoại lai.
Có nhiều phương pháp để xử lý giá trị ngoại lai trong dữ liệu. Tùy thuộc vào tình huống và dữ liệu cụ thể, phương pháp nào cũng có thể đúng hoặc sai. Điều quan trọng là bạn đọc phải phân tích các tình huống có thể liên quan đến giá trị ngoại lai. Đôi khi việc phân tích các giá trị ngoại lai này còn giúp bạn có những hiểu biết hơn về dữ liệu và tối ưu công việc phân tích của bạn.
- Phương pháp đơn giản nhất và cũng thường cho hiệu quả thấp nhất đó là loại bỏ các quan sát, hoặc biến có chứa giá trị ngoại lai. Phương pháp này chỉ có ý nghĩa khi bạn có số lượng quan sát đủ lớn và các giá trị bị coi là ngoại lai không có có ý nghĩa trong xác định phân phối xác suất của từng biến.
- Thay thế giá trị ngoại lai bằng một giá trị khác: bạn đọc có thể thay thế giá trị ngoại lai bằng giá trị có ý nghĩa hơn như giá trị Q0 hoặc Q4 của phân phối xác suất, hoặc cũng có thể thay thế giá trị ngoại lai bằng giá trị trung bình, trung vị, hoặc mode của phân phối. Đây là phương pháp đơn giản, dễ sử dụng và thường cho hiệu quả tốt hơn so với phương pháp xóa quan sát.
- Phương pháp sau cùng và cũng là phương pháp đòi hỏi kỹ thuật phức tạp nhất đó là coi giá trị ngoại lai như một giá trị không quan sát được, sau đó xây dựng mô hình để dự đoán cho giá trị ngoại lai.
Các phương pháp thay thế giá trị ngoại lai và hoặc dự đoán giá trị ngoại lai dựa trên mô hình do tương tự như các phương pháp xử lý dữ liệu không quan sát được nên sẽ được trình bày ở phần sau của chương sách.
5.3.4 Xử lý giá trị không quan sát được.
Khi dữ liệu có giá trị không quan sát được, cách xử lý đơn giản nhất là xóa các quan sát hoặc xóa các biến chứa các giá trị đó. Nếu bạn đọc gặp dữ liệu mà trong đó có một hoặc một số quan sát mà đa số các giá trị trong đó là \(NA\), trong khi tất cả các quan sát còn lại đều không có chứa \(NA\), thì cách xử lý xóa đi quan sát có giá trị \(NA\) là giải pháp hợp lý nhất. Ví dụ như chúng ta có dữ liệu về thông tin của sinh viên của một lớp như sau
| MSV | Name | Age | Gender | Height (cm) | Weight (kg) |
|---|---|---|---|---|---|
| MSV00001 | Lý Văn Thắng | 30 | Nam | 176 | 68 |
| MSV43241 | Nguyễn Văn An | 19 | Nam | 169 | 72 |
| MSV65432 | Lê Thị Loan | 25 | Nữ | 155 | 48 |
| MSV34001 | Trần Mạnh Cường | 15 | Nam | 175 | 150 |
| MSV33789 | Nguyễn Thị Thu Thủy | NA | NA | NA | NA |
Quan sát tương ứng với mã sinh viên “MSV33789” ngoài thông tin về tên sinh viên, các thông tin khác đều không quan sát được. Ngoài sinh viên này, các sinh viên còn lại đều có đầy đủ thông tin. Trong trường hợp này phương pháp xử lý hiệu quả nhất là xóa sinh viên “MSV33789” khỏi dữ liệu trước khi phân tích.
Khi các giá trị không quan sát được tập trung ở một số quan sát (hàng) hoặc tập trung ở một số biến (cột), chúng ta nói rằng các giá trị không quan sát được một cách không ngẫu nhiên (Missing value not at random hay MNAR).
| MSV | Name | Age | Gender | Height (cm) | Weight (kg) | GPA |
|---|---|---|---|---|---|---|
| MSV00001 | Lý Văn Thắng | 30 | Nam | 176 | 68 | 3.25 |
| MSV43241 | Nguyễn Văn An | 19 | Nam | 169 | 72 | NA |
| MSV65432 | Lê Thị Loan | 25 | Nữ | 155 | 48 | NA |
| MSV34001 | Trần Mạnh Cường | 15 | Nam | 175 | 150 | NA |
Dữ liệu ở trên là ví dụ khác về việc giá trị không quan sát được xuất hiện một cách không ngẫu nhiên. Có thể thấy rằng trong cột \(GPA\) đa số các giá trị là không quan sát được. Mọi phân tích liên quan đến giá trị của biến này sẽ không có ý nghĩa, do đó cách tốt nhất là xóa cột này ra khỏi dữ liệu. Để xóa các quan sát có chứa giá trị không quan sát được ra khỏi dữ liệu, bạn đọc có thể sử dụng hàm drop_na() của thư viện tidyr. Để xóa một cột khỏi dữ liệu, bạn đọc có thể coi dữ liệu (một \(data.frame\) hoặc \(tibble\)) như là một \(list\) và gán giá trị của biến đó bằng \(NULL\)
# Du lieu là một tibble hoặc một data.frame có tên là dat
dat<-drop_na(dat) # Xóa các quan sát (hàng) có giá trị NA ra khỏi dữ liệu
dat$ten_cot<-NULL # xóa cột có tên là ten_cot ra khoi du lieuNếu giá trị không quan sát được tập trung vào một số quan sát thì xóa quan sát sẽ không làm ảnh hưởng đến kết quả phân tích. Dữ liệu chúng ta thường gặp sẽ có các giá trị không quán sát được nằm rải rác ở các cột không theo một quy tắc nào. Chúng tôi muốn nói đến trường hợp dữ liệu không quan sát được xuất hiện một cách hoàn toàn ngẫu nhiên (Missing completely at random hay MCAR). Khi gặp trường hợp này nếu xóa đi các quan sát có \(NA\), tỷ lệ quan sát bị xóa đi sẽ là đáng kể.
Để minh họa rõ hơn cho vấn đề này, và để đánh giá hiệu quả của các phương pháp xử lý giá trị \(NA\) trong các phần sau, chúng tôi sẽ sử dụng dữ liệu \(mpg\) của thư viện \(ggplot2\). Đây là dữ liệu có 234 quan sát và 11 biến. Dữ liệu mô tả mức độ tiêu hao nhiên liệu của các loại xe oto thương mại đang bán trên thị trường trong hai năm 1999 và 2008. Dữ liệu không có giá trị \(NA\) nhưng chúng ta sẽ thêm các giá trị không quan sát được vào dữ liệu một các ngẫu nhiên. Sau đó dữ liệu chính xác sẽ được sử dụng để đánh giá phương pháp xử lý giá trị không quan sát được. Bạn đọc cần đọc mô tả về dữ liệu \(mpg\), ý nghĩa của các biến sau đó sử dụng đoạn câu lệnh dưới đây để thêm giá trị \(NA\) vào trong dữ liệu một cách ngẫu nhiên. Chúng ta sẽ gọi tên dữ liệu mới là \(na.mpg\) để phân biệt với dữ liệu ban đầu.
# Tạo dữ liệu mới giống như dữ liệu ban mpg
na.mpg<-mpg
# Định dạng lại format các cột kiểu biến rời rạc thành kiểu factor
chiso<- !(names(na.mpg) %in% c("displ", "cty", "hwy"))
na.mpg[,chiso]<-lapply(na.mpg[,chiso], as.factor)%>%as.data.frame()
# Viết hàm số để thêm giá trị NA vào một véc-tơ
## hàm số có ý nghĩa là thêm vào véc-tơ x các giá trị NA một cách ngẫu nhiên
## tỷ lệ giá trị NA được thêm vào bằng tùy biến na.rate
rd.add<-function(x, na.rate){
n<-length(x)
k<-round(n*na.rate)
ind<-sample(1:n,k,replace=FALSE)
x[ind]<-NA
return(x)
}
# Thêm giá trị NA vào các cột NGOẠI TRỪ ba cột
## Cột nhà sản xuất: manufacturer
## Cột loại xe: model
## Cột năm sản xuất
## tỷ lệ thêm NA một cách ngẫu nhiên vào các cột là 2%
chiso<- !(names(na.mpg) %in% c("manufacturer", "model", "year"))
set.seed(12)
na.mpg[,chiso]<-as.data.frame(lapply(na.mpg[,chiso], rd.add, na.rate = 0.02))
# Xem mỗi cột có bao nhiêu giá trị NA
sapply(na.mpg, f<-function(x) sum(is.na(x)))## manufacturer model displ year cyl trans
## 0 0 5 0 5 5
## drv cty hwy fl class
## 5 5 5 5 5
Chúng ta thấy rằng có 8/11 cột có giá trị \(NA\), cột có 5 giá trị \(NA\) xuất hiện một cách ngẫu nhiên trên tổng số 234 giá trị (tỷ lệ khoảng 2%). Tuy nhiên số quan sát có chứa \(NA\) lại lớn hơn 2% rất nhiều. Hàm drop_na() trong thư viện \(tidyr\) sẽ xóa các quan sát có giá trị \(NA\) ra khỏi dữ liệu. Chúng ta có thể tính được sau khi xóa tỷ lệ dữ liệu còn giữ lại là bao nhiêu:
## [1] 0.8461538
Có thể thấy nếu 2% dữ liệu không quan sát được ở mỗi cột nếu chúng ta xóa các quan sát có giá trị \(NA\), tỷ lệ dữ liệu còn lại là khoảng 85%. Chúng ta có thể thử tăng tỷ lệ giá trị không quan sát được trên mỗi cột lên thành 3%, 5%, 10%, 20%, 30% và quan sát tỷ lệ dữ liệu còn lại sau khi xóa:
| Tỷ lệ NA | Tỷ lệ xóa | Tỷ lệ còn lại |
|---|---|---|
| 2% | 15% | 85% |
| 3% | 21% | 79% |
| 5% | 34% | 66% |
| 10% | 57% | 43% |
| 20% | 84% | 16% |
| 30% | 94% | 6% |
Chúng ta có thể thấy rằng xóa quan sát có giá trị \(NA\) không phải là một giải pháp hiệu quả khi giá trị \(NA\) xuất hiện trong hầu hết các cột. Từ kết quả trên có thể thấy rằng khi tỷ lệ \(NA\) là 5% trở lên ở mỗi cột trong số 8/11 cột của dữ liệu, chúng ta phải xóa đi khoảng 35% số quan sát để dữ liệu không còn \(NA\). Tỷ lệ dữ liệu xóa đi lớn như vậy sẽ ảnh hưởng lớn đến kết quả của phân tích.
Các phương pháp xử lý giá trị không quan sát được trong trường hợp này là thay thế giá trị \(NA\) bằng các giá trị thích hợp. Phương pháp đơn giản nhất đó là giả thiết các cột chứa giá trị không quan sát được độc lập với nhau và sử dụng các giá trị đặc trưng của cột dữ liệu tương ứng để thay thế cho giá trị \(NA\). Các phương pháp phức tạp hơn cân nhắc mối liên hệ giữa các cột dữ liệu và xây dựng các thuật toán để tìm giá trị tối ưu thay thế cho các giá trị không quan sát được nằm trong tất cả các cột. Mỗi phương pháp đều có ưu nhược điểm và chúng tôi thường thử cả hai hướng tiếp cận sau đó đánh giá hiệu quả dựa trên kết quả phân tích.
5.3.4.1 Thay thế giá trị không quan sát được bằng trung bình, trung vị hoặc mode.
Với giả thiết rằng cột chứa giá trị không quan sát được không có mối liên hệ đến các cột còn lại, chúng ta sẽ sử dụng một trong các giá trị như trung bình (mean), trung vị (median), hoặc mode của các giá trị quan sát được để thay thế cho các giá trị không quan sát được.
Giá trị trung bình thường được sử dụng để thay thế cho các giá trị không quan sát được cho véc-tơ kiểu số liên tục và phân phối của các giá trị không có đuôi dài.
Giá trị trung vị, là giá trị tại ngưỡng xác suất 50%, thường được sử dụng để thay thế cho các giá trị không quan sát được cho véc-tơ kiểu số liên tục và véc-tơ có đuôi dài. Giá trị trung vị có ưu điểm là ít bị ảnh hưởng bởi các giá trị ngoại lai và không bị thay đổi sau các bước biến đổi dữ liệu bằng các hàm đơn điệu.
Giá trị mode, là giá trị mà hàm mật độ có xác suất cao nhất, có thể dùng cho cả véc-tơ kiểu số liên tục hoặc véc-tơ kiểu biến rời rạc. Trong trường hợp véc-tơ kiểu số liên tục, bạn đọc cần phải ước lượng hàm mật độ nên giá trị mode sẽ còn phụ thuộc vào phương pháp tiếp cận của người phân tích.
Để thay thế giá trị không quan sát được bằng một giá trị khác, bạn đọc có thể sử dụng hàm na_if() của thư viện dplyr, hàm replace_na của thư viện tidyr, hoặc cũng có thể tự xây dựng hàm số của mình. Để đơn giản hóa, chúng ta giả sử rằng sẽ luôn luôn thay thế giá trị trung vị khi gặp véc-tơ kiểu số liên tục và giá trị mode khi gặp véc-tơ kiểu biến rời rạc.
my_mode<-function(x){ # tự định nghĩa hàm mode cho véc-tơ x
names(which.max(table(x)))
}
my_fillna_1<-function(x){ # tự định nghĩa cách thay thế giá trị NA phương pháp thứ nhất
if(is.numeric(x)){
x[is.na(x)]<-median(x,na.rm=TRUE)
} else {
x[is.na(x)]<-my_mode(x)
}
return(x)
}
mpg_1<-lapply(na.mpg, my_fillna_1)%>%as.data.frame()| displ đúng | displ thay thế | hwy đúng | hwy thay thế | cty đúng | cty thay thế |
|---|---|---|---|---|---|
| 4.0 | 3.3 | 16 | 25 | 17 | 17 |
| 5.4 | 3.3 | 24 | 25 | 11 | 17 |
| 3.8 | 3.3 | 12 | 25 | 15 | 17 |
| 2.7 | 3.3 | 27 | 25 | 18 | 17 |
| 1.8 | 3.3 | 20 | 25 | 26 | 17 |
Giá trị thật của các biến kiểu factor và giá trị dùng để thay thế được tổng kết trong bảng phía dưới
| cyl đúng | cyl thay thế | trans đúng | trans thay thế | drv đúng | drv thay thế | fl đúng | fl thay thế | class đúng | class thay thế |
|---|---|---|---|---|---|---|---|---|---|
| 4 | 4 | auto(l6) | auto(l4) | f | 4 | e | r | suv | suv |
| 8 | 4 | manual(m5) | auto(l4) | f | 4 | r | r | pickup | suv |
| 8 | 4 | manual(m6) | auto(l4) | f | 4 | p | r | suv | suv |
| 4 | 4 | auto(l4) | auto(l4) | 4 | 4 | r | r | subcompact | suv |
| 6 | 4 | manual(m6) | auto(l4) | f | 4 | p | r | pickup | suv |
Chúng ta có thể thấy rằng việc thay thế giá trị \(NA\) trong các véc_tơ kiểu biến liên tục bằng giá trị median là khá hiệu quả. Về tổng thể, giá trị median không cách quá xa so với giá trị thật.
Thay thế các giá trị không quan sát được trong các véc-tơ kiểu biến rời rạc bằng giá trị mode không cho kết quả tốt trong trường hợp này. Nguyên nhân là do giá trị mode trong các biến rời rạc không chiếm ưu thế so với các giá trị khác. Chẳng hạn như biến \(drv\) bị dự đoán sai 4/5 kết quả do biến này có 2 giá trị mode.
5.3.4.2 Thay thế giá trị không quan sát được bằng một mẫu ngẫu nhiên.
Vẫn với giả thiết rằng cột chứa giá trị không quan sát được không có mối liên hệ đến các cột còn lại, chúng ta sẽ sử dụng phép lấy mẫu ngẫu nhiên từ các giá trị quan sát được để thay thế cho các giá trị \(NA\). Hàm sample() là hàm số có sẵn trong R được sử dụng để sinh ngẫu nhiên. Để lấy ra \(k\) số ngẫu nhiên từ một véc-tơ \(x\) ban đầu, chúng ta viết câu lệnh như sau
sample(x,size = k, replace = TRUE) Tùy biến \(replace\) nhận giá trị bằng \(TRUE\) có ý nghĩa là giá trị ngẫu nhiên được lấy ra từ véc-tơ \(x\) có thể được lấy lặp lại. Chúng ta tự định nghĩa hàm fill_na_2() để thay thế giá trị ngẫu nhiên trong một véc-tơ \(x\) bằng các giá trị ngẫu nhiên được lấy từ \(x\) như sau
my_fillna_2<-function(x){ # tự định nghĩa cách thay thế giá trị NA, phương pháp thứ 2
ind<-is.na(x) # véc-tơ kiểu logic, nhận giá trị TRUE tại các vị trí NA
k<-sum(ind)
x[ind]<-sample(x[!ind],k,replace = TRUE)
return(x)
}
set.seed(12)
mpg_1<-lapply(na.mpg, my_fillna_2)%>%as.data.frame()| displ đúng | displ thay thế | hwy đúng | hwy thay thế | cty đúng | cty thay thế |
|---|---|---|---|---|---|
| 4.0 | 4.7 | 16 | 17 | 17 | 17 |
| 5.4 | 4.0 | 24 | 14 | 11 | 21 |
| 3.8 | 4.0 | 12 | 19 | 15 | 15 |
| 2.7 | 4.0 | 27 | 19 | 18 | 18 |
| 1.8 | 4.0 | 20 | 17 | 26 | 13 |
Có thể thấy rằng biến \(year\) là biến gần như không có mối liên hệ đến các biến khác. Khi chúng ta xây dựng mô hình trên dữ liệu, việc xóa bỏ các biến không cần thiết ra khỏi mô hình là rất quan trọng vì các biến này có thể gây ra nhiễu cho mô hình và giảm khả năng dự đoán.
Còn quá sớm để nói đến xây dựng mô hình trên dữ liệu \(mpg\) như thế nào. Bạn đọc nên hiểu
Giá trị thật của các biến kiểu factor và giá trị dùng để thay thế được tổng kết trong bảng phía dưới
| cyl đúng | cyl thay thế | trans đúng | trans thay thế | drv đúng | drv thay thế | fl đúng | fl thay thế | class đúng | class thay thế |
|---|---|---|---|---|---|---|---|---|---|
| 4 | 4 | auto(l6) | manual(m5) | f | f | e | r | suv | compact |
| 8 | 8 | manual(m5) | auto(l4) | f | f | r | r | pickup | compact |
| 8 | 4 | manual(m6) | auto(l5) | f | f | p | p | suv | pickup |
| 4 | 6 | auto(l4) | auto(l4) | 4 | f | r | r | subcompact | midsize |
| 6 | 6 | manual(m6) | auto(l5) | f | f | p | r | pickup | midsize |
Hiệu quả của phương pháp lấy mẫu ngẫu nhiên so với phương pháp sử dụng các giá trị trung vị hoặc mode là không rõ ràng. Tuy nhiên phương pháp này có nhược điểm lớn nhất đó là giá trị được sử dụng để thay thế là ngẫu nhiên nên có khả năng sẽ làm cho dữ liệu bị sai lệch.
5.3.4.3 Thay thế giá trị không quan sát được bằng cách xây dựng mô hình.
Giả thiết cột chứa giá trị không quan sát được không có mối liên hệ đến các cột còn lại là một giả thiết không thực tế. Các cột dữ liệu luôn luôn ít nhiều có mối liên hệ với nhau, hay nói theo khái niệm của xác suất - thống kê thì giá trị trong các cột dữ liệu thường không độc lập với nhau (Not independent hoặc dependent). Làm thế nào để biết hai cột dữ liệu bất kỳ là độc lập hay phụ thuộc là một câu hỏi không dễ có câu trả lời. Do đây là một vấn đề khó và vượt quá phạm vi của cuốn sách nên chúng tôi chỉ trình bày các phương pháp được công nhận rộng rãi. Để kiểm tra hai cột dữ liệu có độc lập hay không, bạn đọc hãy sử dụng các kiểm định như sau:
Kiểm định Khi-bình phương khi cả hai biến đều là biến rời rạc.
Kiểm định hệ số tương quan Person, hệ số tương quan Spearman, và hệ số tương quan Kendall khi cả hai biến đều là biến liên tục.
Sử dụng phân tích phương sai (hay còn gọi là anova test) trong trường hợp một biến là rời rạc và một biến là liên tục.
Chi tiết của các kiểm định này được trình bày ở phần phụ lục của chương.
Để thực hiện kiểm định Khi-bình phương trong R, chúng ta sử dụng hàm chisq.test(). Để kiểm ra hai biến \(year\) và \(drv\) có mối liên hệ hay không, chúng ta thực hiện như sau
chisq.test(na.mpg$year,na.mpg$drv)##
## Pearson's Chi-squared test
##
## data: na.mpg$year and na.mpg$drv
## X-squared = 1.689, df = 2, p-value = 0.4298
Giá trị \(p-value\) bằng 42% nghĩa là xác suất bác bỏ giả thiết hai biến \(year\) và \(drv\) độc lập là \(100\% - 42\% = 58\%\). Thông thường, mức xác suất bác bỏ giả thiết độc lập thường được chọn ở mức \(95\%\) hoặc thậm chí \(99\%\). Do xác suất bác bỏ giả thiết độc lập là thấp nên trong trường hợp này có thể đưa ra kết luận rằng hai biến \(year\) và \(drv\) là không liên quan đến nhau. Tương tự, để kiểm ra hai biến \(drv\) và \(cyl\) có mối liên hệ hay không, chúng ta thực hiện như sau
chisq.test(na.mpg$drv, na.mpg$cyl)##
## Pearson's Chi-squared test
##
## data: na.mpg$drv and na.mpg$cyl
## X-squared = 90.288, df = 6, p-value < 2.2e-16
Do xác suất bác bỏ giả thiết độc lập (\(1-10^{-16}\)) là xấp xỉ \(100\%\) nên trong trường hợp này có thể đưa ra kết luận rằng hai biến \(drv\) và \(cyl\) là không độc lập.
Để kiểm định hệ số tương quan giữa hai biến liên tục chúng ta sử dụng hàm cor.test(). Tùy biến \(method\) nhận giá trị “pearson”, “kendall”, hoặc “spearman” tương ứng với các hệ số tương quan Pearson, hệ số tương quan Kendall hoặc hệ số tương quan Spearman. Chúng ta kiểm định sự độc lập giữa hai biến \(displ\) và \(hwy\) như sau
cor.test(na.mpg$displ, na.mpg$hwy, method = "pearson")##
## Pearson's product-moment correlation
##
## data: na.mpg$displ and na.mpg$hwy
## t = -17.743, df = 223, p-value < 2.2e-16
## alternative hypothesis: true correlation is not equal to 0
## 95 percent confidence interval:
## -0.8143893 -0.7048317
## sample estimates:
## cor
## -0.765092
cor.test(na.mpg$displ, na.mpg$hwy, method = "kendall")##
## Kendall's rank correlation tau
##
## data: na.mpg$displ and na.mpg$hwy
## z = -13.857, p-value < 2.2e-16
## alternative hypothesis: true tau is not equal to 0
## sample estimates:
## tau
## -0.6534741
cor.test(na.mpg$displ, na.mpg$hwy, method = "spearman")##
## Spearman's rank correlation rho
##
## data: na.mpg$displ and na.mpg$hwy
## S = 3467012, p-value < 2.2e-16
## alternative hypothesis: true rho is not equal to 0
## sample estimates:
## rho
## -0.8262809
Kiểm định cả ba hệ số tương quan đều cho xác suất bác bỏ giả thiết hai biến độc lập là xấp xỉ 100%. Nói một cách khác có thể khẳng định hai biến \(displ\) và \(hwy\) là có sự phụ thuộc.
Sau cùng, để kiểm định sự phụ thuộc giữa một biến rời rạc và một biến liên tục, chúng ta sử dụng phân tích phương sai. Hàm số để thực hiện phân tích phương sai trong R là hàm aov(). Chúng ta kiểm định sự phụ thuộc giữa \(hwy\) và \(cyl\) như sau
## Df Sum Sq Mean Sq F value Pr(>F)
## cyl 3 4479 1492.9 101.6 <2e-16 ***
## Residuals 220 3233 14.7
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 10 observations deleted due to missingness
Xác suất bác bỏ giả thiết giá trị trung bình của biến \(hwy\) bằng nhau theo các nhóm của biến \(cyl\) là xấp xỉ \(100\%\) hay nói một cách khác \(hwy\) và \(cyl\) là có mối liên hệ.
Để xem xét một cách tổng thể mối liên hệ giữa các biến trong dữ liệu \(na.mpg\), bạn đọc có thể sử dụng kiểm định phù hợp với từng cặp biến và lưu xác suất bác bỏ giả thiết độc lập vào một ma trận. Hàm số ind_check() được tự xây dựng có đầu vào là một dữ liệu (một tibble hoặc một data.frame) và đầu ra là một ma trận cho biết xác suất bác bỏ giả thiết độc lập của từng cặp biến như thế nào. Bạn đọc có thể xem câu lệnh của hàm số này ở phần phụ lục của chương.
Ma trận thể hiện xác suất bác bỏ giả thiết độc lập giữa từng cặp biến trong dữ liệu \(na.mpg\) như sau
| manufacturer | model | displ | year | cyl | trans | drv | cty | hwy | fl | class | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| manufacturer | 1.00 | 1 | 1.00 | 0.13 | 1.00 | 1 | 1.00 | 1.00 | 1 | 1.00 | 1.00 |
| model | 1.00 | 1 | 1.00 | 0.00 | 1.00 | 1 | 1.00 | 1.00 | 1 | 1.00 | 1.00 |
| displ | 1.00 | 1 | 1.00 | 0.96 | 1.00 | 1 | 1.00 | 1.00 | 1 | 0.92 | 1.00 |
| year | 0.13 | 0 | 0.96 | 1.00 | 0.99 | 1 | 0.57 | 0.41 | 0 | 1.00 | 0.01 |
| cyl | 1.00 | 1 | 1.00 | 0.99 | 1.00 | 1 | 1.00 | 1.00 | 1 | 0.84 | 1.00 |
| trans | 1.00 | 1 | 1.00 | 1.00 | 1.00 | 1 | 1.00 | 1.00 | 1 | 1.00 | 1.00 |
| drv | 1.00 | 1 | 1.00 | 0.57 | 1.00 | 1 | 1.00 | 1.00 | 1 | 0.32 | 1.00 |
| cty | 1.00 | 1 | 1.00 | 0.41 | 1.00 | 1 | 1.00 | 1.00 | 1 | 1.00 | 1.00 |
| hwy | 1.00 | 1 | 1.00 | 0.00 | 1.00 | 1 | 1.00 | 1.00 | 1 | 1.00 | 1.00 |
| fl | 1.00 | 1 | 0.92 | 1.00 | 0.84 | 1 | 0.32 | 1.00 | 1 | 1.00 | 1.00 |
| class | 1.00 | 1 | 1.00 | 0.01 | 1.00 | 1 | 1.00 | 1.00 | 1 | 1.00 | 0.00 |
Có thể thấy biến \(year\) gần như không có mối liên hệ đến các biến khác. Khi xây dựng mô hình trên dữ liệu, sự xuất hiện của các biến không cần thiết sẽ thêm nhiễu và làm giảm chất lượng của mô hình. Trong trường hợp này, chúng ta sẽ loại bỏ biến \(year\) trước khi thực hiện dự đoán cho các giá trị không quan sát được.
Phương pháp để xây dựng mô hình dự đoán cho các giá trị không biết là sử dụng thuật toán “rừng ngẫu nhiên”. Đây là một thuật toán nâng cao của mô hình dạng cây quyết định. Còn quá sớm để nói về mô hình này, bạn đọc chỉ cần hiểu rằng chúng ta sẽ dựa vào các giá trị quan sát được để xây dựng mô hình (một hàm số \(f\)) mà biến có giá trị \(NA\) phụ thuộc vào biến không có giá trị \(NA\) để đưa ra dự đoán. Thư viện “missForest” hỗ trợ chúng ta làm việc này. Bạn đọc có thể cài thư viện sau đó sử dụng hàm missForest(). Quy trình điền giá trị \(NA\) vào dữ liệu \(na.mpg\) chỉ cần 1 dòng lệnh!
library(missForest)
### Thời gian chạy mất khoảng 1-2 phút
model<-missForest(select(na.mpg,-year), maxiter = 200, ntree = 100)
mpg_1<-model$ximp # Dữ liệu mpg_1 là dữ liệu sau khi thay thế NAGiá trị thật của các biến kiểu số và các giá trị thay thế trong bảng dưới đây
| displ đúng | displ thay thế | hwy đúng | hwy thay thế | cty đúng | cty thay thế |
|---|---|---|---|---|---|
| 4.0 | 3.999844 | 16 | 15.87147 | 17 | 16.37833 |
| 5.4 | 4.672718 | 24 | 24.43083 | 11 | 10.99517 |
| 3.8 | 3.788511 | 12 | 13.57500 | 15 | 14.23717 |
| 2.7 | 2.945000 | 27 | 25.85814 | 18 | 17.97967 |
| 1.8 | 1.929000 | 20 | 19.25812 | 26 | 25.90011 |
Giá trị thật của các biến kiểu factor và giá trị dùng để thay thế được tổng kết trong bảng phía dưới
| cyl đúng | cyl thay thế | trans đúng | trans thay thế | drv đúng | drv thay thế | fl đúng | fl thay thế | class đúng | class thay thế |
|---|---|---|---|---|---|---|---|---|---|
| 4 | 4 | auto(l6) | auto(l5) | f | f | e | r | suv | suv |
| 8 | 8 | manual(m5) | manual(m5) | f | f | r | r | pickup | pickup |
| 8 | 8 | manual(m6) | auto(l4) | f | f | p | r | suv | suv |
| 4 | 4 | auto(l4) | auto(l5) | 4 | 4 | r | r | subcompact | subcompact |
| 6 | 6 | manual(m6) | auto(s6) | f | f | p | r | pickup | pickup |
Bạn đọc có thể thấy rằng ngoài biến \(trans\), đa số các biến còn lại đều được dự đoán khá chính xác bằng thuật toán rừng ngẫu nhiên. Chúng ta sẽ thảo luận về mô hình này trong chương thảo luận về mô hình cây quyết định.
6 Biến đổi dữ liệu bằng thư viện \(dplyr\)
Dữ liệu trước khi đưa vào trực quan hóa hoặc xây dựng mô hình hiếm khi có được định dạng chính xác mà bạn đọc mong muốn. Thông thường, bạn đọc sẽ cần tạo thêm một số biến, hoặc có thể bạn đọc chỉ muốn đổi tên các biến, hoặc sắp xếp lại các quan sát, hoặc tổng hợp dữ liệu để làm cho dữ liệu dễ dàng xử lý tiếp. Bạn đọc sẽ học cách thực hiện tất cả những phép biến đổi đó trong phần này. Nội dung chủ yếu của phần này sẽ hướng dẫn bạn đọc cách chuyển đổi dữ liệu của mình bằng cách sử dụng thư viện \(dplyr\) và tập dữ liệu \(gapminder\) - dữ liệu về sức khỏe và thu nhập của các quốc gia trên thế giới từ năm 1960 đến năm 2016.
Thư viện \(dplyr\) là một thư viện nằm trong thư viện tổng hợp \(tidyverse\), bạn đọc có thể gọi thư viện \(tidyverse\) hoặc trực tiếp thư viện \(dplyr\) lên cửa sổ đang làm việc. Bạn đọc cũng nên lưu ý rằng trong thư viện \(dplyr\) có một số hàm trùng tên với các hàm có sẵn trong R, chẳng hạn như hàm \(filter()\) hoặc hàm \(lag()\). Tuy nhiên R ưu tiên \(dplyr\) trước các thư viện có sẵn nên bạn đọc sẽ nhận được thông báo khi gọi thư viện này.
Dữ liệu \(gapminder\) là dữ liệu đã được sử dụng trong phần tiền xử lý dữ liệu. Bạn đọc lưu ý thực hiện các bước tiền xử lý dữ liệu trước khi chạy các câu lệnh biến đổi dữ liệu. Mỗi phần tiếp theo sẽ giới thiệu một hàm quan trọng của thư viện \(dplyr\) và các tham số của hàm đó. Sau cùng chúng tôi sẽ giới thiệu về cách sử dụng pipe (\(%\>\%\)) để kết nối các hàm với nhau trong một câu lệnh duy nhất.
6.1 Thêm biến bằng hàm \(mutate()\)
Khi bạn đọc muốn thêm các cột khác vào một dữ liệu được tính toán từ các cột hiện có, bạn đọc có thể sử dụng hàm \(mutate()\). Hàm \(mutate()\) luôn thêm cột vào sau cột cuối cùng trong dữ liệu hiện có. Khi bạn muốn thêm cột vào một vị trí cụ thể, bạn có thể sử dụng tùy biến
- \(.after = ten\_cot\) trong đó \(ten\_cot\) là tên cột phía trước cột mà bạn đọc muốn thêm vào.
- \(.before = ten\_cot\) trong đó \(ten\_cot\) là tên cột phía sau cột mà bạn đọc muốn thêm vào.
mytib<-as.tibble(gapminder) # mytib là dữ liệu kiểu tibble
mutate(mytib, gdp_per_capita = gdp/population) # thêm cột có tên là gdp_per_capita## # A tibble: 10,545 × 10
## country year infan…¹ life_…² ferti…³ popul…⁴ gdp conti…⁵ region gdp_p…⁶
## <fct> <int> <dbl> <dbl> <dbl> <dbl> <dbl> <fct> <fct> <dbl>
## 1 Albania 1960 115. 62.9 6.19 1.64e6 NA Europe South… NA
## 2 Algeria 1960 148. 47.5 7.65 1.11e7 1.38e10 Africa North… 1243.
## 3 Angola 1960 208 36.0 7.32 5.27e6 NA Africa Middl… NA
## 4 Antigu… 1960 NA 63.0 4.43 5.47e4 NA Americ… Carib… NA
## 5 Argent… 1960 59.9 65.4 3.11 2.06e7 1.08e11 Americ… South… 5254.
## 6 Armenia 1960 NA 66.9 4.55 1.87e6 NA Asia Weste… NA
## 7 Aruba 1960 NA 65.7 4.82 5.42e4 NA Americ… Carib… NA
## 8 Austra… 1960 20.3 70.9 3.45 1.03e7 9.67e10 Oceania Austr… 9393.
## 9 Austria 1960 37.3 68.8 2.7 7.07e6 5.24e10 Europe Weste… 7415.
## 10 Azerba… 1960 NA 61.3 5.57 3.90e6 NA Asia Weste… NA
## # … with 10,535 more rows, and abbreviated variable names ¹infant_mortality,
## # ²life_expectancy, ³fertility, ⁴population, ⁵continent, ⁶gdp_per_capita
Câu lệnh trên thêm cột có tên là \(gdp\_per\_capita\) được tính bằng tổng thu nhập quốc nội (\(gdp\)) chia cho dân số của quốc gia đó. Nếu bạn đọc muốn cột mới được thêm vào ngay sau cột \(gdp\), hãy sử dụng thêm tùy biến \(.after\)
mutate(mytib, gdp_per_capita = gdp/population,.after = gdp) # thêm cột có tên là gdp_per_capita## # A tibble: 10,545 × 10
## country year infan…¹ life_…² ferti…³ popul…⁴ gdp gdp_p…⁵ conti…⁶ region
## <fct> <int> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <fct> <fct>
## 1 Albania 1960 115. 62.9 6.19 1.64e6 NA NA Europe South…
## 2 Algeria 1960 148. 47.5 7.65 1.11e7 1.38e10 1243. Africa North…
## 3 Angola 1960 208 36.0 7.32 5.27e6 NA NA Africa Middl…
## 4 Antigu… 1960 NA 63.0 4.43 5.47e4 NA NA Americ… Carib…
## 5 Argent… 1960 59.9 65.4 3.11 2.06e7 1.08e11 5254. Americ… South…
## 6 Armenia 1960 NA 66.9 4.55 1.87e6 NA NA Asia Weste…
## 7 Aruba 1960 NA 65.7 4.82 5.42e4 NA NA Americ… Carib…
## 8 Austra… 1960 20.3 70.9 3.45 1.03e7 9.67e10 9393. Oceania Austr…
## 9 Austria 1960 37.3 68.8 2.7 7.07e6 5.24e10 7415. Europe Weste…
## 10 Azerba… 1960 NA 61.3 5.57 3.90e6 NA NA Asia Weste…
## # … with 10,535 more rows, and abbreviated variable names ¹infant_mortality,
## # ²life_expectancy, ³fertility, ⁴population, ⁵gdp_per_capita, ⁶continent
Một biến thể khác của hàm \(mutate()\) là \(transmute()\). Hàm \(transmute()\) khác \(mutate()\) ở chỗ là chỉ giữ lại các cột mới được tạo thành
transmute(mytib, gdp_per_capita = gdp/population) # data mới chỉ có cột gdp_per_capita## # A tibble: 10,545 × 1
## gdp_per_capita
## <dbl>
## 1 NA
## 2 1243.
## 3 NA
## 4 NA
## 5 5254.
## 6 NA
## 7 NA
## 8 9393.
## 9 7415.
## 10 NA
## # … with 10,535 more rows
6.2 Lựa chọn cột bằng hàm \(select()\)
Khi dữ liệu có quá nhiều cột và bạn đọc chỉ muốn sử dụng một số cột nhất định để phân tích, bạn đọc hãy sửa dụng hàm \(select()\). Điều quan trọng nhất khi sử dụng hàm \(select()\) là bạn đọc cần phải gọi tên đúng các cột (biến) mà mình muốn lựa chọn.
select(mytib, year, gdp, population) # lấy ra các cột year, gdp, population## # A tibble: 10,545 × 3
## year gdp population
## <int> <dbl> <dbl>
## 1 1960 NA 1636054
## 2 1960 13828152297 11124892
## 3 1960 NA 5270844
## 4 1960 NA 54681
## 5 1960 108322326649 20619075
## 6 1960 NA 1867396
## 7 1960 NA 54208
## 8 1960 96677859364 10292328
## 9 1960 52392699681 7065525
## 10 1960 NA 3897889
## # … with 10,535 more rows
Hàm \(select\) cũng có thể được sử dụng để thay đổi tên cột. Chẳng hạn bạn đọc muốn tên các cột mới tương ứng là \("Year"\), \("GDP"\) và \("Population"\)
select(mytib, Year = year, Gdp = gdp, Population = population) # lấy ra và đổi tên các cột year, gdp, population## # A tibble: 10,545 × 3
## Year Gdp Population
## <int> <dbl> <dbl>
## 1 1960 NA 1636054
## 2 1960 13828152297 11124892
## 3 1960 NA 5270844
## 4 1960 NA 54681
## 5 1960 108322326649 20619075
## 6 1960 NA 1867396
## 7 1960 NA 54208
## 8 1960 96677859364 10292328
## 9 1960 52392699681 7065525
## 10 1960 NA 3897889
## # … with 10,535 more rows
Khi dữ liệu có quá nhiều cột và việc gọi tên chính xác các cột làm cho câu lệnh \(select()\) quá dài, bạn đọc có thể lựa chọn các cột đứng liền nhau bằng cách sau
select(mytib, year, gdp:population) # ## # A tibble: 10,545 × 3
## year gdp population
## <int> <dbl> <dbl>
## 1 1960 NA 1636054
## 2 1960 13828152297 11124892
## 3 1960 NA 5270844
## 4 1960 NA 54681
## 5 1960 108322326649 20619075
## 6 1960 NA 1867396
## 7 1960 NA 54208
## 8 1960 96677859364 10292328
## 9 1960 52392699681 7065525
## 10 1960 NA 3897889
## # … with 10,535 more rows
Câu lệnh trên có ý nghĩa là lấy ra cột \(year\) và tất cả các cột nằm giữa cột \(gdp\) và cột \(population\).
Bạn đọc không nhớ chính xác tên cột muốn lấy ra thì có thể sử dụng các tùy chọn sau:
\(starts\_with()\): lấy ra các cột có tên bắt đầu bằng một chuỗi ký tự nào đó.
\(ends\_with()\): lấy ra các cột có tên kết thúc bằng một chuỗi ký tự nào đó.
\(contains()\): lấy ra các cột có tên chứa một chuỗi ký tự nào đó.
\(matches()\): lấy ra các cột có tên khớp với một xxxxxxxxxxxxxxxxxxx nào đó
mytib1<-mutate(mytib, gdp_per_capita = gdp/population) # mytib1 là tibble có thêm cột tên là gdp_per_capita
select(mytib1, contains("gdp")) # lấy ra tất cả các cột có tên chứa "gdp"## # A tibble: 10,545 × 2
## gdp gdp_per_capita
## <dbl> <dbl>
## 1 NA NA
## 2 13828152297 1243.
## 3 NA NA
## 4 NA NA
## 5 108322326649 5254.
## 6 NA NA
## 7 NA NA
## 8 96677859364 9393.
## 9 52392699681 7415.
## 10 NA NA
## # … with 10,535 more rows
Hàm \(select()\) ngoài ý nghĩa là lựa chọn cột còn có ý nghĩa là loại bỏ một số cột nào đó khỏi dữ liệu hiện thời. Để loại bỏ cột, bạn đọc chỉ cần thêm dấu “-” vào trước tên cột.
select(mytib1, - starts_with("gdp")) # bỏ đi tất cả các cột có tên chứa "gdp"## # A tibble: 10,545 × 8
## country year infant_mor…¹ life_…² ferti…³ popul…⁴ conti…⁵ region
## <fct> <int> <dbl> <dbl> <dbl> <dbl> <fct> <fct>
## 1 Albania 1960 115. 62.9 6.19 1.64e6 Europe South…
## 2 Algeria 1960 148. 47.5 7.65 1.11e7 Africa North…
## 3 Angola 1960 208 36.0 7.32 5.27e6 Africa Middl…
## 4 Antigua and Barbuda 1960 NA 63.0 4.43 5.47e4 Americ… Carib…
## 5 Argentina 1960 59.9 65.4 3.11 2.06e7 Americ… South…
## 6 Armenia 1960 NA 66.9 4.55 1.87e6 Asia Weste…
## 7 Aruba 1960 NA 65.7 4.82 5.42e4 Americ… Carib…
## 8 Australia 1960 20.3 70.9 3.45 1.03e7 Oceania Austr…
## 9 Austria 1960 37.3 68.8 2.7 7.07e6 Europe Weste…
## 10 Azerbaijan 1960 NA 61.3 5.57 3.90e6 Asia Weste…
## # … with 10,535 more rows, and abbreviated variable names ¹infant_mortality,
## # ²life_expectancy, ³fertility, ⁴population, ⁵continent
6.3 Lọc quan sát bằng hàm filter()
Hàm filter() cho phép bạn đọc lọc các quan sát dựa trên giá trị của các cột. Do có một số thư viện trong R sử dụng hàm \(filter()\) với mục đích khác nhau và chúng tôi không chắc chắn cửa sổ R bạn đọc đang sử dụng đang có sẵn các thư viện nào nên chúng tôi đổi tên cho hàm \(filter()\) của thư viện \(dplyr\) thành \(dfilter()\) phương pháp mượn tham số:
dfilter<-function(...) dplyr::filter(...)Cách hoạt động của hàm \(filter()\) như sau: bạn đọc muốn lấy dữ liệu của năm 2010 từ dữ liệu \(gapminder\), chúng ta sử dụng hàm \(filter()\) như sau
dfilter(mytib, year == 2010) # chỉ lấy các quan sát có year là 2010## # A tibble: 185 × 9
## country year infan…¹ life_…² ferti…³ popul…⁴ gdp conti…⁵ region
## <fct> <int> <dbl> <dbl> <dbl> <dbl> <dbl> <fct> <fct>
## 1 Albania 2010 14.8 77.2 1.74 2.90e6 6.14e 9 Europe South…
## 2 Algeria 2010 23.5 76 2.82 3.60e7 7.92e10 Africa North…
## 3 Angola 2010 110. 57.6 6.22 2.12e7 2.61e10 Africa Middl…
## 4 Antigua and Ba… 2010 7.7 75.8 2.13 8.72e4 8.37e 8 Americ… Carib…
## 5 Argentina 2010 13 75.8 2.22 4.12e7 4.34e11 Americ… South…
## 6 Armenia 2010 16.1 73 1.55 2.96e6 4.10e 9 Asia Weste…
## 7 Aruba 2010 NA 75.1 1.7 1.02e5 NA Americ… Carib…
## 8 Australia 2010 4.1 82 1.89 2.22e7 5.63e11 Oceania Austr…
## 9 Austria 2010 3.6 80.5 1.44 8.39e6 2.24e11 Europe Weste…
## 10 Azerbaijan 2010 33.9 70.1 1.97 9.10e6 2.12e10 Asia Weste…
## # … with 175 more rows, and abbreviated variable names ¹infant_mortality,
## # ²life_expectancy, ³fertility, ⁴population, ⁵continent
Sau khi chạy câu lệnh trên, R sẽ tạo thành một \(tibble()\) mới chỉ bao gồm các quan sát có giá trị cột \(year\) là 2010. Lưu ý rằng nếu bạn đọc muốn lưu lại giá trị sau mỗi lần thực hiện tính toán hãy gán giá trị trả lại vào một đối tượng mới. \(filter()\) có thể được thực hiện khi sử dụng nhiều cột, chẳng hạn như bạn đọc muốn lọc các quan sát của năm 2010 của các quốc gia Châu Âu
dfilter(mytib, year == 2010, continent == "Europe") ## # A tibble: 39 × 9
## country year infan…¹ life_…² ferti…³ popul…⁴ gdp conti…⁵ region
## <fct> <int> <dbl> <dbl> <dbl> <dbl> <dbl> <fct> <fct>
## 1 Albania 2010 14.8 77.2 1.74 2.90e6 6.14e 9 Europe South…
## 2 Austria 2010 3.6 80.5 1.44 8.39e6 2.24e11 Europe Weste…
## 3 Belarus 2010 4.7 70.2 1.46 9.49e6 2.60e10 Europe Easte…
## 4 Belgium 2010 3.6 80.1 1.84 1.09e7 2.67e11 Europe Weste…
## 5 Bosnia and Herz… 2010 6.4 77.9 1.24 3.84e6 8.21e 9 Europe South…
## 6 Bulgaria 2010 11.2 73.7 1.49 7.41e6 1.92e10 Europe Easte…
## 7 Croatia 2010 4.6 76.7 1.47 4.32e6 2.80e10 Europe South…
## 8 Czech Republic 2010 3.4 77.5 1.5 1.05e7 8.21e10 Europe Easte…
## 9 Denmark 2010 3.3 79.4 1.88 5.55e6 1.69e11 Europe North…
## 10 Estonia 2010 3.6 76.4 1.63 1.33e6 8.01e 9 Europe North…
## # … with 29 more rows, and abbreviated variable names ¹infant_mortality,
## # ²life_expectancy, ³fertility, ⁴population, ⁵continent
6.4 Sắp xếp dữ liệu bằng hàm arrange()
Hàm arrange() sắp xếp các hàng của dữ liệu theo thứ tự tăng dần. Nguyên tắc sắp xếp cũng tương tự như hàm sort() khi làm trên véc-tơ, nghĩa là hàm bạn đọc có thể sắp xếp dữ liệu dựa theo bất kỳ kiểu dữ liệu nào. Chẳng hạn như để sắp xếp dữ liệu \(gapminder\) theo thứ tự tăng dần theo năm, theo Châu lục, và sau cùng là theo vùng, bạn đọc sử dụng câu lệnh như sau
arrange(mytib, year, continent, region) ## # A tibble: 10,545 × 9
## country year infant_mort…¹ life_…² ferti…³ popul…⁴ gdp conti…⁵ region
## <fct> <int> <dbl> <dbl> <dbl> <dbl> <dbl> <fct> <fct>
## 1 Burundi 1960 145. 40.6 6.95 2.79e6 3.41e8 Africa Easte…
## 2 Comoros 1960 200 44.0 6.79 1.89e5 NA Africa Easte…
## 3 Djibouti 1960 NA 45.8 6.46 8.36e4 NA Africa Easte…
## 4 Eritrea 1960 NA 39.0 6.9 1.41e6 NA Africa Easte…
## 5 Ethiopia 1960 162 37.7 6.88 2.22e7 NA Africa Easte…
## 6 Kenya 1960 119. 47.4 7.95 8.11e6 2.12e9 Africa Easte…
## 7 Madagascar 1960 112 42.0 7.3 5.10e6 2.09e9 Africa Easte…
## 8 Malawi 1960 218. 38.5 6.91 3.62e6 3.48e8 Africa Easte…
## 9 Mauritius 1960 67.8 58.7 6.17 6.60e5 NA Africa Easte…
## 10 Mozambique 1960 183 38.2 6.6 7.49e6 NA Africa Easte…
## # … with 10,535 more rows, and abbreviated variable names ¹infant_mortality,
## # ²life_expectancy, ³fertility, ⁴population, ⁵continent
Nếu muốn sắp xếp dữ liệu theo thứ tự giảm dần, nếu cột dữ liệu là kiểu số, bạn đọc chỉ cần thêm dấu “-” trước tên biến. Trong trường hợp cột dữ liệu kiểu bất kỳ, bạn đọc sử dụng hàm \(desc()\)
arrange(mytib, year, desc(continent), desc(region), -gdp) # tăng dần theo năm, giảm dần theo continent, region, gdp## # A tibble: 10,545 × 9
## country year infan…¹ life_…² ferti…³ popul…⁴ gdp conti…⁵ region
## <fct> <int> <dbl> <dbl> <dbl> <dbl> <dbl> <fct> <fct>
## 1 French Polynesia 1960 NA 56.3 5.66 78083 NA Oceania Polyn…
## 2 Samoa 1960 92 51.4 7.65 108645 NA Oceania Polyn…
## 3 Tonga 1960 NA 61.2 7.36 61600 NA Oceania Polyn…
## 4 Kiribati 1960 NA 45.8 6.95 41234 NA Oceania Micro…
## 5 Micronesia, Fed… 1960 NA 56.8 6.93 44539 NA Oceania Micro…
## 6 Papua New Guinea 1960 135. 38.6 6.28 1966957 8.37e8 Oceania Melan…
## 7 Fiji 1960 54 55.7 6.46 393383 4.37e8 Oceania Melan…
## 8 New Caledonia 1960 NA 56.4 5.22 78058 NA Oceania Melan…
## 9 Solomon Islands 1960 132. 50.6 6.39 117869 NA Oceania Melan…
## 10 Vanuatu 1960 107. 46.0 7.2 63701 NA Oceania Melan…
## # … with 10,535 more rows, and abbreviated variable names ¹infant_mortality,
## # ²life_expectancy, ³fertility, ⁴population, ⁵continent
Lưu ý rằng nếu trong cột dữ liệu sử dụng để sắp xếp có giá trị \(NA\) thì các giá trị này luôn được sắp xếp xuống phía dưới của dữ liệu.
6.5 Kết hợp các hàm bằng toán tử pipe (\%>\%)
Trước khi giới thiệu các hàm khác dùng để biến đổi dữ liệu của thư viện \(dplyr\), chúng tôi muốn giới thiệu đến bạn đọc toán tử pipe (\(\%>\%\)). Đây là một công cụ vô cùng hữu hiệu khi bạn đọc thực hiện một chuỗi các phép biến đổi dữ liệu. Toán tử pipe được mượn từ toán học khi nói đến việc sử dụng các hàm số nối tiếp nhau. Pipe trong thư viện \(dplyr\) cũng có ý nghĩa tương tự khi bạn đọc sử dụng một chuỗi các hàm của thư viện này nhằm biến đổi dữ liệu. Ví dụ như khi bạn đọc muốn lấy ra từ dữ liệu \(gapminder\) ba quốc gia có thu nhập bình quân đầu người cao nhất trong năm 2000, bạn sẽ cần các phép biến đổi sau: - Thứ nhất: thêm cột thu nhập bình quân đầu người (sử dụng hàm \(mutate()\)) - Thứ hai: lọc dữ liệu theo năm, chỉ lấy dữ liệu của năm 2000. (sử dụng hàm \(filter()\)) - Thứ ba: lựa chọn cột tên quốc gia và cột thu nhập bình quân đầu người (sử dụng hàm \(select()\)) - Thứ tư: sắp xếp dữ liệu theo cột thu nhập bình quân đầu người, thứ tự sắp xếp là giảm dần (sử dụng hàm \(arrange()\)) - Thứ năm: lấy ra ba hàng đầu tiên của dữ liệu (sử dụng hàm \(head()\))
Nếu không sử dụng toán tử \(pipe\), sau mỗi bước ở trên bạn đọc sẽ phải lưu kết quả và gọi lại kết quả vào bước kế tiếp:
mytib1<-mutate(mytib,gdp_per_capita = gdp/population) # bước thứ nhất
mytib1<-dfilter(mytib1, year == 2010) # bước thứ hai
mytib1<-select(mytib1, country, gdp_per_capita) # bước thứ ba
mytib1<-arrange(mytib1, desc(gdp_per_capita)) # bước thứ tư
head(mytib1,3) # bước thứ năm## # A tibble: 3 × 2
## country gdp_per_capita
## <fct> <dbl>
## 1 Luxembourg 52210.
## 2 Japan 40013.
## 3 Norway 39954.
Thay vì phải lưu lại dữ liệu sau mỗi lần sử dụng biến đổi dữ liệu và gọi lại kết quả để sử dụng cho bước tiếp theo, bạn đọc có thể sử dụng toán tử \(pipe\) như sau
mytib%>%mutate(gdp_per_capita = gdp/population)%>%
dfilter(year == 2010)%>%
select(country, gdp_per_capita)%>%
arrange(desc(gdp_per_capita)) %>%
head(3)## # A tibble: 3 × 2
## country gdp_per_capita
## <fct> <dbl>
## 1 Luxembourg 52210.
## 2 Japan 40013.
## 3 Norway 39954.
Kết quả thu được hoàn toàn tương tự như trên tuy nhiên câu lệnh đã rõ ràng hơn rất nhiều. Từ phần này của cuốn sách, mọi phép biến đổi trên dữ liệu chúng tôi sẽ luôn luôn ưu tiên sử dụng toán tử \(pipe\) vì sự đơn giản và rõ ràng của các câu lệnh.
6.6 Tổng hợp dữ liệu bằng \(group\_by()\) và \(summarise()\)
Hàm \(group\_by()\) là một công cụ hữu hiệu trong tổng hợp dữ liệu và tính toán theo nhóm. Chẳng hạn như từ dữ liệu \(gapminder\) bạn đọc muốn biết tổng thu nhập quốc dân (\(gdp\)) của một quốc gia là cao hay thấp so với tổng thu nhập quốc dân trung bình của châu lục (\(continent\)) trong năm tương ứng (\(year\)), bạn đọc cần phải thực hiện các thao tác sau: - Bước 1: Nhóm dữ liệu lại theo châu lục và theo năm - Bước 2: Tính tổng thu nhập quốc dân của châu lục trong năm đó, bỏ qua các nước không có quan sát - Bước 3: Đếm xem có bao nhiêu giá trị có quan sát - Bước 4: Lấy kết quả ở bước 2 chia cho kết quả của bước 3. - Bước 5: So sánh gdp của quốc gia với gdp trung bình của châu lục trong năm đó
Những bước như trên có thể được thực hiện một cách đơn giản thông qua hàm \(group\_by\) như sau
mytib%>%group_by(continent, year) %>%
mutate(gdp_year_continent = mean(gdp,na.rm=TRUE))%>% # thêm cột gdp bình quân của châu lục theo năm
ungroup()%>% # để dữ liệu trở lại trạng thái ban đầu (trước khi group)
mutate(gdp_level = ifelse(gdp > gdp_year_continent, "High", "Low"))## # A tibble: 10,545 × 11
## country year infan…¹ life_…² ferti…³ popul…⁴ gdp conti…⁵ region gdp_y…⁶
## <fct> <int> <dbl> <dbl> <dbl> <dbl> <dbl> <fct> <fct> <dbl>
## 1 Albania 1960 115. 62.9 6.19 1.64e6 NA Europe South… 1.11e11
## 2 Algeria 1960 148. 47.5 7.65 1.11e7 1.38e10 Africa North… 3.65e 9
## 3 Angola 1960 208 36.0 7.32 5.27e6 NA Africa Middl… 3.65e 9
## 4 Antigu… 1960 NA 63.0 4.43 5.47e4 NA Americ… Carib… 1.15e11
## 5 Argent… 1960 59.9 65.4 3.11 2.06e7 1.08e11 Americ… South… 1.15e11
## 6 Armenia 1960 NA 66.9 4.55 1.87e6 NA Asia Weste… 6.12e10
## 7 Aruba 1960 NA 65.7 4.82 5.42e4 NA Americ… Carib… 1.15e11
## 8 Austra… 1960 20.3 70.9 3.45 1.03e7 9.67e10 Oceania Austr… 3.27e10
## 9 Austria 1960 37.3 68.8 2.7 7.07e6 5.24e10 Europe Weste… 1.11e11
## 10 Azerba… 1960 NA 61.3 5.57 3.90e6 NA Asia Weste… 6.12e10
## # … with 10,535 more rows, 1 more variable: gdp_level <chr>, and abbreviated
## # variable names ¹infant_mortality, ²life_expectancy, ³fertility,
## # ⁴population, ⁵continent, ⁶gdp_year_continent
Nếu đoạn câu lệnh trên không sử dụng câu lệnh \(group\_by\), hàm \(mean()\) trong câu lệnh \(mutate()\) sẽ thực hiên tính toán cho toàn bộ véc-tơ \(gdp\) của dữ liệu \(gapminder\). Nghĩa là cột mới được hình thành sẽ có giá trị giống nhau với tất cả các quan sát. Sau khi sử dụng hàm \(group\_by()\), mỗi khi chúng ta sử dụng một hàm tính toán trên một cột dữ liệu khác, cột dữ liệu đó sẽ được tính toán cho từng nhóm được định nghĩa bởi hàm \(group\_by\). Trong đoạn lệnh ở trên, hàm \(mean()\) sẽ tính giá trị trung bình của véc-tơ \(gdp\) cho từng châu lục theo từng năm. Thật vậy, chúng ta có thể kiểm tra giá trị ở hàng đầu tiên của cột \(gdp\_year\_continent\) (tương ứng với Albania - năm 1960 - châu Âu) sẽ là giá trị trung bình của véc-tơ \(gdp\) của các nước châu Âu trong năm 1960:
## [1] NA
Hàm \(group\_by()\) kết hợp với \(summarise()\) sẽ tạo thành một dữ liệu mới mà mỗi hàng sẽ tương đương với một nhóm được quy định bởi hàm \(group\_by()\).
## # A tibble: 285 × 3
## # Groups: continent [5]
## continent year gdp_year_continent
## <fct> <int> <dbl>
## 1 Africa 1960 3652247577.
## 2 Africa 1961 3642863976.
## 3 Africa 1962 3781167783.
## 4 Africa 1963 4107185952.
## 5 Africa 1964 4342684293.
## 6 Africa 1965 4618242478.
## 7 Africa 1966 4562215808.
## 8 Africa 1967 4592913225.
## 9 Africa 1968 4814235499.
## 10 Africa 1969 5157970620.
## # … with 275 more rows
Bạn đọc có thể thấy rằng dữ liệu mới (dưới dạng một \(tibble\)) được tạo thành, mỗi hàng là một châu lục trong một năm, với ba cột bao gồm hai cột được quy định bởi hàm \(group\_by()\) là \(continent\), \(year\) và cột \(gdp\_year\_continent\) mới được tạo thành từ hàm \(summarise()\).
7 Trực quan hóa dữ liệu
p<-gapminder%>%filter(year<=2010)%>%
# AESTHETIC MAPPING
ggplot(aes(x=fertility,y=life_expectancy,size = population, fill= continent))+
# TAO DO THI SCATTERPLOT
geom_point(shape=21,alpha=0.6)+
# THAY DOI TITLE CUA DO THI, TRUC X, TRUC Y
labs(title = 'Năm: {as.integer(frame_time)}',
y = "Tuổi thọ trung bình",
x = "Tỷ lệ sinh trên mỗi phụ nữ")+
#GIOI HAN LAI GIA TRI TREN X,Y
xlim(0,10)+ylim(20,90)+
# SCALE LAI SIZE (POPULATION)
scale_size(range = c(1*2, 20*2)) +
# SCALE LAI MAU SAC THE0 DAI MAU "SET1" CUA BREWER
scale_color_brewer(palette = "Set1")+
# LAM TITLE THAY DOI THEO NAM
transition_time(year)+
#SIZE & FONT CHU
theme(,
plot.title = element_text(size = 20*2),
axis.title.x = element_text(size = 20*2),
axis.title.y = element_text(size = 20*2),
legend.text = element_text(size = 20*2,margin = margin(r = 30*2, unit = "pt")),
legend.title = element_text(size = 20*2),
# legend.text=element_text(size=20*2),
)
#legend.key.size = element_rect(size = rel(1.5)),
# TAO DO THI DANG DONG
animate(p, renderer = gifski_renderer(),
width = 1600, #pixel chieu rong
height = 1600) # pixel chieu cao
##
## Attaching package: 'dplyr'
## The following object is masked from 'package:gridExtra':
##
## combine
## The following object is masked from 'package:kableExtra':
##
## group_rows
## The following objects are masked from 'package:stats':
##
## filter, lag
## The following objects are masked from 'package:base':
##
## intersect, setdiff, setequal, union
##
## Attaching package: 'lubridate'
## The following objects are masked from 'package:base':
##
## date, intersect, setdiff, union
##
## Attaching package: 'plotly'
## The following object is masked from 'package:ggplot2':
##
## last_plot
## The following object is masked from 'package:stats':
##
## filter
## The following object is masked from 'package:graphics':
##
## layout
8 Trực quan hóa dữ liệu
Trực quan hóa dữ liệu là nghệ thuật mô tả dữ liệu thông qua việc sử dụng đồ họa và hình ảnh như các biểu đồ, sơ đồ, và cả hình ảnh động hoặc hình ảnh tương tác. Trực quan hóa dữ liệu là một phương pháp truyền đạt thông tin một cách trực quan và dễ hiểu từ người quản lý dữ liệu đến người tiếp nhận. Trực quan hóa giúp mô tả các mối quan hệ dữ liệu phức tạp, các thông tin chuyên sâu, và cả các vấn đề bất thường ẩn chứa trong dữ liệu.
Tại sao lại cần trực quan hóa dữ liệu ? Thứ nhất là do não bộ của con người sẽ cho phản ứng đối với hình ảnh, màu sắc, kích thước, khoảng cách, … tốt hơn nhiều so với các ký hiệu và con số. Thứ hai là do dữ liệu mà chúng ta phải đối mặt trong thời đại ngày nay ngày càng lớn và phức tạp. Trực quan hóa là phương pháp hiệu quả nhất để tìm ra các giá trị ẩn chứa bên trong dữ liệu. Đây chính là điểm khiến kỹ năng trực quan hóa dữ liệu được đánh giá là kỹ năng quan trọng nhất đối với những người phân tích dữ liệu.
Có nhiều công cụ để trực quan hóa dữ liệu. Tiêu biểu phải kể đến Power BI và Tableau. Đây là hai công cụ thân thiện với người dùng, cho phép người dùng tạo bảng điều khiển và báo cáo tương tác một cách nhanh chóng và dễ dàng. Cả hai đều có giao diện kiểu kéo và thả chuột giúp dễ dàng tạo hình ảnh trực quan mà không cần bất kỳ kỹ năng lập trình nào.
R sử dụng thư viện \(ggplot2\) để trực quan hóa dữ liệu. Sẽ là không dễ dàng cho người mới bắt đầu vẽ được đồ thị bằng \(ggplot2\). Điểm mạnh của \(ggplot2\) so với các công cụ như Power BI hay Tableau là cho phép người dùng tạo các hình ảnh có khả năng tùy biến cao. \(ggplot2\) là một lựa chọn phù hợp dành cho các nhà phân tích dữ liệu, những người cảm thấy hứng thú với việc viết các câu lệnh để tạo ra các hình ảnh trực quan phức tạp, và đúng theo ý muốn của mình. Với một chút kinh nghiệm về Power BI và Tableau, cùng với nhiều hơn một chút kinh nghiệm về \(ggplot2\), chúng tôi cho rằng bạn đọc nên làm quen với cả hai cách trực quan hóa dữ liệu. Khi bạn phải tạo các báo cáo trực quan trong một thời gian ngắn, Power BI hay Tableau sẽ là lựa chọn tối ưu. Khi bạn muốn vẽ những hình ảnh phức tạp, có tính cá nhân cao, và bạn có thời gian để làm việc đó, hãy sử dụng \(ggplot2\).
8.1 Giới thiệu về \(ggplot2\)
\(ggplot2\) là một thư viện để trực quan hóa dữ liệu trong R. Ngoài \(ggplot2\), bạn đọc cũng có thể sử dụng các đồ thị cơ bản của R, hoặc sử dụng các thư viện khác như \(lattice\) để vẽ đồ thị. Tuy nhiên, không giống như hầu hết các công cụ khác, \(ggplot2\) trực quan hóa dữ liệu dựa trên Ngữ pháp của đồ thị (Wilkinson 2005). Hai chữ \(gg\) bắt đầu có nghĩa là Grammar of Graphic hay Ngữ pháp của đồ thị. Ngữ pháp cho phép bạn đọc vẽ đồ thị bằng cách kết hợp các cấu phần độc lập lại với nhau. Đây chính là điểm mạnh của \(ggplot2\). Thay vì bị giới hạn ở các bộ đồ thị đã được xác định trước, bạn đọc có thể tạo đồ thị mới phù hợp với mục tiêu của mình. Ý tưởng phải học ngữ pháp để vẽ đồ thị có thể làm cho bạn đọc cảm thấy nản chí, nhưng sự thật là ngữ pháp của \(ggplot2\) thực sự dễ học. Chỉ có một số nguyên tắc cốt lõi đơn giản và có rất ít trường hợp đặc biệt. Khi đã thông thạo Ngữ pháp của đồ thị, ngoài tạo ra những đồ thị quen thuộc, bạn đọc còn có thể tạo ra những đồ thị mới hơn, đẹp hơn và mang tính cách riêng. Bạn đọc có thể gặp khó khăn một chút thời gian ban đầu nhưng chúng tôi tin rằng khi đã quen với \(ggplot2\) thì sẽ rất ít bạn đọc muốn quay lại với các công cụ trực quan hóa dữ liệu khác.
Hãy thử xem một chút \(ggplot2\) trực quan hóa dữ liệu như thế nào. Chúng ta sẽ bắt đầu với một dữ liệu có tên là \(murders\) trong thư viện \(dslabs\). Giả sử bạn muốn du lịch đến Mỹ nhưng bạn lo ngại về việc cho phép sử dụng súng ở quốc gia này và bạn muốn biết ở những bang nào có tỷ lệ số vụ sát nhân bằng súng cao. Dữ liệu \(murders\) là dữ liệu do FBI cung cấp về số vụ sát nhân bằng súng tại các bang của nước Mỹ vào năm 2010. Do bạn đọc đã biết về \(data.frame()\), nên bạn có thể tìm hiểu về dữ liệu bằng các hàm như head(), str(), view()
## state abb region population total
## 1 Alabama AL South 4779736 135
## 2 Alaska AK West 710231 19
## 3 Arizona AZ West 6392017 232
## 4 Arkansas AR South 2915918 93
## 5 California CA West 37253956 1257
## 6 Colorado CO West 5029196 65
Thật khó để có thể có được cái nhìn tổng thể về dữ liệu nếu chỉ nhìn vào các bảng, các con số, ký hiệu như trên. Thay vì sử dụng con số, bạn đọc có thể trình bày dữ liệu \(murders\) dưới dạng một đồ thị rải điểm (scatter plot) như sau

Chúng tôi đã sử dụng một vài kỹ thuật biến đổi dữ liệu để vẽ đồ thị ở trên:
Do các biến total (tổng số vụ sát nhân) và biến population (dân số của mỗi bang) đều có đuôi dài, nghĩa là có nhiều điểm tập trung ở khu vực trung tâm, và một số ít điểm tập trung ở phía đuôi bên phải, do đó thay vì sử dụng chính xác giá trị của các biến trên đồ thị, các điểm của đồ thị rải điểm sẽ phân bố không đồng đều. Giá trị hiển thị trên đồ thị đã được điều chính lại theo hàm \(log()\) cơ số 10.
Chúng tôi thêm vào một đường thẳng tuyến tính (đường kẻ màu xám đi qua trung tâm) để mô tả mối quan hệ chung giữa hai biến \(total\) và \(population\).
Dựa trên đồ thị rải điểm ở trên, bạn đọc có thể đưa ra được ngay các nhận xét như sau
Bang nào có dân số càng cao thì số vụ sát nhân bằng súng càng nhiều.
Hầu hết các bang nằm phía trên đường trung bình là các bang ở miền Nam (màu đỏ).
Các vùng còn lại không có sự phân biệt rõ ràng.
Bang “District of Columbia” là bang nằm cao hơn hẳn so với đường trung bình, và cũng là bang có tỷ lệ số vụ sát nhân bằng súng cao nhất.
Bang California có tổng số vụ sát nhân bằng súng lớn nhất, nhưng tỷ lệ số vụ sát nhân bằng súng trên đầu người chỉ bằng mức trung bình chung.
Không dễ dàng để đưa ra được các nhận xét như trên nếu chỉ dựa trên quan sát con số và dữ liệu. Thay vì biểu diễn dưới dạng con số, chúng ta có thể đưa ra nhiều phân tích có ý nghĩa về dữ liệu khi sử dụng đồ thị như trên.
Wilkinson (2005) giới thiệu khái niệm Ngữ pháp đồ thị để mô tả các thành phần cơ bản làm nền tảng cho tất cả các đồ thị sử dụng và cách các thành phần tương tác trong mô tả dữ liệu. Ngữ pháp đồ thị là mô tả chính xác nhất cho câu hỏi đồ thị trực quan hóa dữ liệu là gì? Thư viện \(ggplot2\) được Wickham giới thiệu vào 2009 xây dựng dựa trên ngữ pháp đồ thị mà Wilkinson đã đề cập bằng cách tập trung vào việc xây dựng đồ thị dựa trên nhiều lớp (layer). Nhìn chung, ngữ pháp đồ thị cho chúng ta biết quy tắc cho tương ứng các biến của dữ liệu với các thuộc tính thẩm mỹ (các aesthetic attributions) của đối tượng hình ảnh xuất hiện (các geometries). Đồ thị trong \(ggplot2\) cũng có thể bao gồm các mô hình thống kê của dữ liệu và hệ tọa độ mà đồ thị sử dụng. Bạn đọc cũng có thể chia dữ liệu thành các tập hợp con dựa trên các biến rời rạc và mô tả dữ liệu thông qua một nhóm các đồ thị con thông qua kỹ thuật facetting. Sự kết hợp của các thành phần độc lập kể trên tạo nên một đồ thị mô tả dữ liệu.
Bạn đọc không cần phải lo lắng nếu khái niệm Ngữ pháp đồ thị ở trên không có ý nghĩa ngay lập tức. Trong phần sau của cuốn sách, chúng tôi sẽ nói về ngữ pháp đồ thị một cách chi tiết hơn. Bạn sẽ có nhiều cơ hội hơn để tìm hiểu về Ngữ pháp và các sử dụng ngữ pháp để các cấu phần độc lập của một đồ thị hoạt động cùng nhau. Trong phần giới thiệu này, chúng tôi muốn bạn đọc hãy ghi nhớ thành phần độc lập tạo nên một đồ thị cơ bản bao gồm có
Dữ liệu (Data) là dữ liệu hay tập hợp các dữ liệu mà bạn đọc muốn trực quan hóa. Thông thường thì chỉ có một dữ liệu chính mà bạn đọc muốn minh họa cho người tiếp nhận dữ liệu, trong khi các dữ liệu khác được sử dụng với mục đích để mô tả dữ liệu chính. Một ví dụ điển hình của dữ liệu phụ là dữ liệu kiểu bản đồ. Chẳng hạn như khi bạn đọc muốn mô tả về dữ liệu \(murder\), bạn đọc có thể sử dụng dữ liệu về bản đồ nước Mỹ để mô tả tốt hơn về dữ liệu \(murder\).
Hình dạng đồ họa (các \(geometries\) hay viết tắt là các \(geom\)) là những hình dạng đồ họa mà chúng ta muốn nhìn thấy trên đồ thị. Các hình dạng này có thể là các điểm, các thanh, các đường.
Các ánh xạ thẩm mỹ (Aesthetic mapping) là các quy tắc cho tương ứng từ các biến (cột của dữ liệu) đến các thuộc tính thẩm mỹ (aesthetic attribution) của các hình dạng đồ họa. Các thuộc tính thẩm mỹ có thể là hình dạng, màu sắc, độ đậm nhạt, …
Các mô hình hay biến đổi thống kê (statistics hay viết tắt là stats) là các quy tắc tóm tắt dữ liệu, các mô hình xây dựng trên dữ liệu được mô tả dưới dạng một hình dạng đồ họa nhằm tăng tính dễ hiểu cho đồ thị hoặc làm nổi bật một xu thế nào đó. Ví dụ như trong đồ thị rải điểm mô tả dữ liệu \(murder\), chúng tôi đã sử dụng một mô hình tuyến tính mô tả mối quan hệ giữa biến \(total\) và biến \(population\) với mục đích phân loại ra các bang có tỷ lệ số vụ sát nhân bằng súng thấp hơn và các bang có tỷ lệ số vụ sát nhân bằng súng cao hơn so với chung bình chung.
Hệ tọa độ (Cordinate) mô tả cách dữ liệu được trực quan hóa trên mặt phẳng của đồ họa. Đa số các trường hợp chúng ta sẽ sử dụng hệ tọa độ Descartes, nhưng cũng có một số hệ tọa độ khác có thể sử dụng bao gồm tọa độ cực và bản đồ.
Một thành phần mô tả cách dữ liệu được hiển thị là chia nhỏ dữ liệu để mô tả bằng một nhóm các đồ thị thay vì một đồ thị duy nhất được gọi là facetting. Thành phần này thường được sử dụng để mô tả dữ liệu có kích thước lớn và hoặc chúng ta muốn so sánh trực quan dữ liệu ở các nhóm khác nhau.
Thành phần cuối cùng của đồ thị là ngữ cảnh của đồ thị hay các themes. Theme quy định khung hoặc nền mà đồ thị được hiển thị chẳng hạn như kích thước phông chữ hoặc màu nền. Mặc dù các giá trị mặc định trong \(ggplot2\) đã được lựa chọn hợp lý nhưng bạn đọc cũng có thể cần tham khảo các tài liệu tham khảo khác để tạo ra một ngữ cảnh phù hợp hơn cho đồ thị của mình.
Mỗi khi vẽ một đồ thị \(ggplot\), bạn đọc cần tự định nghĩa ít nhất ba thành phần: 1. Dữ liệu; 2. Các hình dạng đồ họa; và 3. Các ánh xạ thẩm mỹ. Các thành phần 5. Hệ tọa độ; và 7. Ngữ cảnh; sẽ được tự động gán cho các giá trị mặc định nếu bạn đọc không quy định trong câu lệnh. Và các thành phần 4. Mô hình; và 6. Facetting; chỉ xuất hiện khi bạn đọc gọi lên trong câu lệnh của mình.
Trước khi đi vào giới thiệu chi tiết các cách tạo nên một đồ thị trực quan hóa, bạn đọc cũng cần biết được các hạn chế khi trực quan hóa dữ liệu bằng \(ggplot2\):
\(ggplot2\) là một thư viện của R nên bạn đọc cần có kỹ năng viết câu lệnh R tương đối thành thạo.
\(ggplot2\) không gợi ý bạn đọc nên sử dụng đồ thị nào khi gặp một dữ liệu cụ thể. Điều đó cũng có nghĩa là bạn đọc cần có một chút kinh nghiệm về trực quan hóa dữ liệu trước khi sử dụng \(ggplot2\).
\(ggplot2\) không được phát triển để vẽ các đồ thị động hay đồ thị tương tác mà chỉ tập trung vào vẽ các đồ thị tĩnh. Muốn vẽ các đồ thị tương tác hay đồ thị động trong \(ggplot2\) bạn đọc phải sử dụng các packages đi kèm như \(gganimate\) hay \(ggplotly\).
Để kết thúc phần giới thiệu về \(ggplot2\), chúng tôi sẽ sử dụng \(ggplot2\) kết hợp với \(gganimate\) để kể một câu chuyện (story telling) về sự phát triển của các quốc gia trên thế giới từ năm 1960 đến năm 2010 thông qua hai khía cạnh là tuổi thọ trung bình và thu nhập bình quân đầu người. Dữ liệu chính được sử dụng là dữ liệu \(gapminder\).
8.2 Tạo một đồ thị \(ggplot2\) cơ bản
Trước khi giới thiệu chi tiết về các thành phần độc lập của đồ thị và cách sử dụng ngữ pháp của đồ thị, chúng tôi nghĩ rằng sẽ tốt hơn nếu bạn đọc bắt đầu vẽ các đồ thị đơn giản bằng cách copy và dán các câu lệnh vẽ đồ thị trước. Sau khi thực thi một vài lần, bạn đọc sẽ có “cảm nhận” được phần nào cách mà một đồ thị của \(ggplot2\) được xây dựng. Dữ liệu chúng tôi sử dụng để trực quan hóa trong suốt chương sách này là dữ liệu \(gapminder\), dữ liệu về sức khỏe và thu nhập của tất cả các quốc gia trên thế giới bắt đầu từ năm 1960 đến năm 2016. Bạn đọc hãy đảm bảo rằng mình đã đọc và hiểu một cách cơ bản về dữ liệu này.

Bạn đọc có thể thấy rằng dữ liệu \(gapminder\) có nhiều giá trị không quan sát được trong năm 2016. Hai cột có tỷ lệ không quan sát được qua các năm lớn là \(infant\_mortality\) và \(gdp\). Riêng biến \(gdp\) là gần như không quan sát được từ năm 2012 đến 2016. Do chỉ sử dụng dữ liệu với mục đích trực quan hóa nên chúng tôi sẽ tiền xử lý dữ liệu một cách đơn giản là xóa các quan sát của các năm 2012 đến 2016. Giá trị không quan sát được từ năm 1960 đến 2012 sẽ được thay thế bằng mô hình rừng ngẫu nhiên. Dữ liệu sau bước tiền xử lý này được gọi là \(gapminder\_1\)
Hàm số để vẽ đồ thị của thư viện \(ggplot2\) là hàm ggplot(). Bạn đọc hãy nhớ rằng ba thành phần bắt buộc phải có của một đồ thị là 1. Dữ liệu; 2. (Ít nhất) Một hình dạng đồ họa; và 3. Ánh xạ thẩm mỹ. Đồ thị dưới đây mô tả hai biến gdp bình quân đầu người và tuổi thọ trung bình của các quốc gia trên thế giới vào năm 2011. Bạn đọc có thể copy các đoạn lệnh ở dưới vào cửa sổ R script và thực hiện giống như các câu lệnh thông thường.
dat<-gapminder%>%filter(year==2011)%>%mutate(gdp_per_capita = gdp/population)
ggplot(dat, aes(x = life_expectancy, y = gdp_per_capita)) + geom_point()
Trong câu lệnh ggplot() ở trên, dữ liệu được đưa vào là \(data.frame\) có tên là \(dat\), hình dạng đồ họa là các điểm trên trục tọa độ Descartes. Hình dạng đồ họa này được gọi bằng hàm geom_point(). Ánh xạ thẩm mỹ được gọi thông qua hàm aes() nằm trong hàm ggplot(). Trong ánh xạ thẩm mỹ ở trên, chúng ta đã cho tương ứng biến \(life\_expectancy\) với giá trị trên trục \(x\) của trục tọa độ Descartes, biến \(gdp\_per\_capita\) với giá trị trên trục \(y\) của trục tọa độ Descartes.
Bạn đọc đã có thể thấy được một vài thông tin phán ánh trên dữ liệu.
Có mối liên hệ đồng biến giữa tuổi thọ trung bình và thu nhập bình quân đầu người. Quốc gia nào có thu nhập bình quân đầu người cao thì tuổi thọ trung bình cũng sẽ cao. Điều này khá hợp lý bởi các quốc gia có thu nhập trung bình cao thường là các nước phát triển có hệ thống chăm sóc sức khỏe tốt, do đó tuổi thọ trung bình cũng sẽ cao.
Mối liên hệ đồng biến nhưng không tuyến tính, thu nhập bình quân đầu người tăng nhanh hơn rất nhiều ro với tuổi thọ trung bình.
Có một vài điểm có khả năng là ngoại lai trong mối liên hệ tuyến tính này. Đây là các quốc gia có mức thu nhập bình quân khá cao (từ 10 nghìn USD - 20 nghìn USD/1 người) nhưng lại có tuổi thọ trung bình không cao. Tuy nhiên chỉ với các thông tin như trên chúng ta không thể đưa ra giải thích cho các giá trị có khả năng cao là ngoại lai này.
Hình dạng đồ họa là những gì mà bạn đọc nhìn thấy trên đồ thị của mình. Khi gọi các hình dạng đồ họa thư viện \(ggplot2\) luôn luôn sử dụng các hàm số bắt đầu bởi geom là viết tắt của \(geometries\). Bạn đọc có thể thử với một vài hình dạng đồ họa quen thuộc như dưới đây
## geom_histogram() sử dụng các thanh để mô tả phân phối của một biến liên tục
ggplot(dat,aes(x = gdp_per_capita))+geom_histogram()
## geom_bar() sử dụng các thanh để mô tả phân phối của một biến rời rạc
ggplot(dat,aes(x = continent))+geom_bar()
## geom_boxplot() sử dụng các hình hộp để mô tả phân phối của biến liên tục
ggplot(dat,aes(x = continent, y = life_expectancy))+geom_boxplot()
## geom_line() sử dụng đường nối các điểm theo thứ tự điểm xuất hiện
dat1<-filter(gapminder, year<=2011, country == "United States")%>%select(year,gdp)
ggplot(dat1,aes(x = year, y = gdp))+geom_line()Còn rất nhiều các hàm \(geom_*()\) khác có thể được sử dụng để tạo đồ thị trong thư viện \(ggplot2\). Bạn đọc có thể xem danh sách các \(geom\) thường sử dụng trong danh sách đính kèm dưới đây.
https://www.maths.usyd.edu.au/u/UG/SM/STAT3022/r/current/Misc/data-visualization-2.1.pdf
Bạn đọc có thể thầy rằng CHEAT SHEET cũng đã có gợi ý cho người sử dụng nên dùng hàm geom_*() nào trong từng trường hợp. Chẳng hạn như geom_point() được khuyên dùng trong trường hợp mô tả hai biến liên tục. Đồng thời, mỗi hàm geom_*() sẽ có một danh sách các thuộc tính thẩm mỹ đi kèm. Đối với geom_point() các thuộc tính thẩm mỹ bao gồm \(x\), \(y\), \(alpha\), \(color\), \(fill\), \(shape\), \(size\), và \(stroke\). Bạn đọc hướng dẫn sử dụng của hàm geom_point() để biết các thuộc tính thẩm mỹ này có ý nghĩa như thế nào. Trong các thuộc tính thẩm mỹ này, \(color\), \(fill\), \(shape\) và \(size\) là các thuộc tính thẩm mỹ xuất hiện ở nhiều hàm geom_*() khác. Đây là các thuộc tính thẩm mỹ thường xuyên được sử dụng để tăng khả năng mô tả dữ liệu của các đồ thị.
Để mô tả tốt hơn mối quan hệ giữa thu nhập bình quân đầu người và tuổi thọ trung bình của các quốc gia trên thế giới, chúng ta cần thêm thông tin vào đồ thị ở trên. Một phương pháp đơn giản để thêm biến khác vào một đồ thị là ánh xạ biến đó đến một trong các thuộc tính thẩm mỹ của đồ thị được vẽ bởi hàm geom_point(). Biến được thêm vào dưới đây là biến \(continent\). Chúng ta sẽ ánh xạ biến đó tương ứng với thuộc tính thẩm mỹ \(color\) như sau
dat<-gapminder%>%filter(year==2011)%>%mutate(gdp_per_capita = gdp/population)
ggplot(dat, aes(x = life_expectancy, y = gdp_per_capita, color = continent)) +
geom_point()
Chúng ta đã có thể đưa ra thêm các phân tích về mối liên hệ giữa tuổi thọ trung bình và thu nhập bình quân. Có sự phân bố không đồng đều về thu nhập bình quân và tuổi thọ trung bình của các quốc gia trên thế giới, đa số các quốc gia Châu Phi (màu đỏ) có thu nhập bình quân đầu người thấp và tuổi thọ trung bình thấp; các quốc gia Châu Âu (màu xanh da trời) có thu nhập bình quân đầu người cao và tuổi thọ trung bình cao. Có sự phân hóa rõ ràng ở Châu Đại Dương và Châu Mỹ, một vài quốc gia nằm trong nhóm các nước có thu nhập cao, tuổi thọ trung bình cao trong khi đa số các quốc gia còn lại nằm trong nhóm thu nhập thấp và tuổi thọ trung bình thấp. Sự phân hóa ở Châu Á không quá rõ ràng.
Có một nguyên tắc là thuộc tính thẩm mỹ \(color\) thường được sử dụng với biến rời rạc và thuộc tính thẩm mỹ \(size\) thường được sử dụng với biến liên tục. Thuộc tính thẩm mỹ \(shape\) chỉ có thể được sử dụng với biến rời rạc, R sẽ báo lỗi nếu bạn ánh xạ một biến liên tục vào \(shape\). Có 21 giá trị khác nhau trong dành cho thuộc tính thẩm mỹ \(shape\) do đó R sẽ có cảnh báo nếu bạn đọc ánh xạ một biến rời rạc có nhiều hơn 21 giá trị.
Đồ thị dưới đây thêm biến \(population\) vào đồ thị bằng cách sử dụng thuộc tính thẩm mỹ \(size\). Bạn đọc hãy luôn nhớ rằng để khai báo ánh xạ thẩm mỹ từ một biến đến một thuộc tính thẩm mỹ, hãy luôn luôn khai báo bên trong hàm aes().
ggplot(dat, aes(x = life_expectancy, y = gdp_per_capita,
color = continent, size = population)) + geom_point(alpha = 0.4)
Tham số \(alpha\) sử dụng trong hàm geom_point() trong trường hợp dữ liệu có nhiều điểm bị trùng lên nhau. Chúng ta sẽ thảo luận kỹ hơn về thuộc tính thẩm mỹ này trong phần sau của chương. Khi thêm biến \(population\) vào có thể làm đồ thị có thêm thông tin, chẳng hạn như bạn đọc có thể nhận ra vị trí của các quốc gia đông dân tiêu biểu như Trung Quốc và Ấn Độ vào năm 2011 vẫn nằm trong nhóm các nước có thu nhập bình quân đầu người thấp; hoặc cũng có thể nhận ra Mỹ và Nhật Bản là các quốc gia nằm ở góc trên bên phải là các nước cũng có dân số tương đối lớn. Tuy nhiên, bạn đọc cũng có thể nhận ra rằng khi cùng sử dụng nhiều thuộc tính thẩm mỹ trên một đồ thị, hiệu quả sẽ không được như mong muốn.
Với các dữ liệu có nhiều quan sát, bạn đọc có thể chia nhỏ dữ liệu thành các nhóm và tạo đồ thị cho từng nhóm.
ggplot(dat, aes(x = life_expectancy, y = gdp_per_capita, size = population)) +
geom_point(alpha = 0.4)+
facet_wrap(~continent)
Sử dụng năm đồ thị có cùng trục tọa độ \(x\) và \(y\) để mô tả mối quan hệ giữa thu nhập bình quân đầu người và tuổi thọ trung bình là rõ ràng hơn rất nhiều so với sử dụng một đồ thị duy nhất và phân biệt bằng màu sắc.
Thành phần cuối cùng mà bạn đọc có thể thêm vào đồ thị \(ggplot2\) là ngữ cảnh, hay các \(themes\). Có một số \(theme\) có sẵn hoặc có các \(theme\) nằm trong các thư viện cài đặt bổ sung. Chúng ta sẽ nói chi tiết về cách tùy chỉnh \(theme\) hoặc tự tạo \(theme\) ở phần sau của chương. Trong đoạn câu lệnh dưới đây, chúng tôi sử dụng theme_minimal() là một \(theme\) có sẵn trong thư viện \(ggplot2\).
ggplot(dat, aes(x = life_expectancy, y = gdp_per_capita, size = population)) +
geom_point(shape = 21, alpha = 0.7, fill = "lightskyblue")+
facet_wrap(~continent)+
# thêm title
labs( title = "Thu nhập bình quân và tuổi thọ trung bình",
subtitle = "Các quốc gia trên thế giới năm 2011")+
xlab("Tuổi thọ trung bình (năm)")+
ylab("Gdp bình quân đầu người (USD)")+
theme_minimal()# thêm ngữ cảnh
Còn một thành phần khác chưa được nhắc đến khi tạo đồ thị là các \(statistics\) hay viết tắt là các \(stats\). Tuy nhiên đây là thành phần phức tạp nhất và liên quan đến các kiến thức về xây dựng mô hình trên dữ liệu nên chúng tôi không sử dụng trong phần này. Mục tiêu của chúng tôi là để bạn đọc làm quen và tự tạo các đồ thị đơn giản bằng các dòng lệnh của thư viện \(ggplot2\).
8.3 Cấu trúc nhiều lớp và ngữ pháp của đồ thị \(ggplot2\).
Cấu trúc theo lớp (layer) của đồ thị \(ggplot2\) giúp cho người phân tích dữ liệu có thể xây dựng đồ thị của mình theo hướng có cấu trúc. Đồ thị trong \(ggplot2\) từ đơn giản đến phức tạp đều được tạo thành từ một hoặc nhiều lớp. Mỗi lớp trong đồ thị có mục tiêu hiển thị khác nhau:
Mục tiêu thứ nhất và cũng là mục tiêu chính, đó là để hiển thị dữ liệu. Luôn luôn có một hoặc một vài lớp (chính) với mục tiêu mô tả dữ liệu thô, mô tả cấu trúc tổng thể và các giá trị ngoại lai của dữ liệu. Lớp này xuất hiện trên tất cả các đồ thị. Trong giai đoạn đầu của quá trình khai phá dữ liệu bằng trực quan hóa, lớp này thường xuất hiện duy nhất. Đơn giản như khi mô tả mỗi quan hệ giữa gdp bình quân đầu người và tuổi thọ trung bình của tất cả các quốc gia trên thế giới, lớp đồ thị được hiển thị bằng hàm
geom_point()là lớp hiển thị dữ liệu.Các lớp có mục tiêu tóm tắt và mô tả ý nghĩa thống kê của dữ liệu. Bằng cách thêm vào đồ thị các mô hình, hoặc hiển thị các dự đoán dựa trên mô hình người tiếp nhận dữ liệu, hoặc bản thân người phân tích dữ liệu sẽ nhận biết được những giá trị bên trong dữ liệu, hoặc những chi tiết mà khi xây dựng mô hình có thể bỏ sót.
Các lớp có mục tiêu thêm vào ngữ cảnh của dữ liệu. Các lớp này hiển thị bối cảnh nền, thêm vào các chú thích giúp mang lại ý nghĩa cho dữ liệu thô hoặc các giá trị tham chiếu nhằm hỗ trợ việc so sánh hoặc đánh giá. Đây thường là lớp cuối cùng được thêm vào trong đồ thị.
Lớp chính của đồ thị có thể bao gồm bảy thành phần độc lập giống như chúng ta đã giới thiệu ở phần đầu. Cấu trúc của các lớp còn lại của đồ thị \(ggplot2\) có thể bao gồm các thành phần sau
Dữ liệu: nếu bạn không khai báo dữ liệu trong mỗi lớp, \(ggplot2\) sẽ sử dụng dữ liệu ban đầu là giá trị mặc định
Ánh xạ thẩm mỹ: được khai báo trong hàm
aes()trong mỗi lớp, nếu không có khai báo \(ggplot2\) sẽ tìm ánh xạ thẩm mỹ trong hàmggplot(). Trong trường hợp không tìm thấy ánh xạ thẩm mỹ nào được khai báo, \(ggplot2\) sử dụng giá trị mặc định.Một hình dạng đồ họa: được gọi từ các hàm
geom_*()Một biến đổi thống kê hoặc một tóm tắt cơ bản của dữ liệu được gọi từ hàm \(stat_*()\)
Vị trí xuất hiện của lớp đó. Chúng ta sẽ thảo luận vấn đề này chi tiết trong phần sau của chương.
Khi đồ thị chỉ có một lớp chính với mục tiêu hiển thị dữ liệu thô, bạn không cần phải am hiểu về ngữ pháp của đồ thị. Bạn đọc chỉ cần khai báo chính xác ánh xạ thẩm mỹ trong hàm aes() để có được kết quả mong muốn. Tuy nhiên khi xây dựng đồ thị có nhiều lớp, bạn đọc cần phải nắm được ngữ pháp để kết hợp các lớp lại với nhau theo ý muốn của bạn.
8.3.1 Ánh xạ thẩm mỹ trong đồ thị có nhiều lớp.
Hãy quan sát ví dụ ở dưới đây, khi bạn muốn thêm vào một đường cong mô tả xu thế mối quan hệ giữa gdp bình quân đầu người và tuổi thọ trung bình của các quốc gia trên thế giới vào năm 2011.
## Hình bên trái
p1<-dat%>%ggplot(aes(x = life_expectancy, y = gdp_per_capita,
color = continent)) +
geom_point(alpha = 0.4)+
geom_smooth(se=FALSE)
## Hình bên phải
p2<-dat%>%ggplot(aes(x = life_expectancy, y = gdp_per_capita))+
geom_point(aes(color = continent), alpha = 0.4) +
geom_smooth(se=FALSE)
## Vẽ p1 và p2 trên cùng một đồ thị
grid.arrange(p1,p2,nrow= 1 , ncol = 2)
Bạn đọc có thể thấy sự khác nhau giữa hai đồ thị là các đường mô tả mối quan hệ giữa tuổi thọ bình quân và gdp bình quân đầu người được xây dựng theo từng lục địa (hình bên trái) và được xây dựng cho tất cả các quốc gia (hình bên phải). Sự khác biệt là do trong hình bên phải, thay vì khai báo ánh xạ thẩm mỹ từ biến \(continent\) tới thuộc tính thẩm mỹ \(color\) trong hàm ggplot(), chúng tôi đã khai báo ánh xạ thẩm mỹ này trog hàm geom_point().
Nếu như hàm geom_point() là lớp chính mô tả dữ liệu thô thì hàm geom_smooth() là lớp phụ được thêm vào nhằm tăng khả năng mô tả của dữ liệu. Các ánh xạ thẩm mỹ được khai báo trong hàm ggplot() cũng giống như các biến toàn cục trong một đồ thị cụ thể, trong khi các ánh xạ thẩm mỹ được khai báo trong các hàm geom_*() giống như khai báo giá trị cho các biến cục bộ trong môi trường của hàm số đó. Các biến cục bộ nếu không được khai báo trong các hàm geom_*() sẽ được tìm trên môi trường toàn cục của hàm ggplot(). Trong trường hợp trong các hàm geom_*() và ggplot() đều không được khai báo giá trị, biến sẽ nhận giá trị mặc định.
Hãy quay trở lại đoạn câu lệnh ở trên. Trong hình bên trái, các thuộc tính thẩm mỹ \(x\), \(y\), và \(color\) được khai báo trong hàm ggplot(); đồng thời trong các hàm geom_point() và geom_smooth() không khai báo các ánh xạ thẩm mỹ; do đó cả hai hàm này đều hiểu các thuộc tính thẩm mỹ \(x\), \(y\), và \(color\) giống như khai báo ban đầu. Trong hình bên phải, hai thuộc tính thẩm mỹ \(x\) và \(y\) được khai báo trong hàm ggplot() trong khi thuộc tính thẩm mỹ \(color\) được khai báo bên trong hàm geom_point(). Do đó, hàm geom_smooth() chỉ hiểu hai thuộc tính thẩm mỹ \(x\) và \(y\) như được khai báo trong ggplot().
| geom_point() | geom_smooth() | |
|---|---|---|
| Hình bên trái | x, y và color | x, y và color |
| Hình bên phải | x, y, và color | x, y |
Hàm geom_smooth() khi thuộc tính thẩm mỹ \(color\) nhận giá trị mặc định sẽ xây dựng một mô hình được gọi là locally estimated scatterplot smoothing hay loess nhằm mô tả mỗi quan hệ giữa biến liên tục \(y\) theo một biến liên tục \(x\). Nếu thuộc tính thẩm mỹ \(color\) được khai báo giá trị tương ứng với một biến rời rạc, geom_smooth() sẽ chia dữ liệu thành các nhóm tương ứng với \(color\) trước khi xây dựng mô hình mà \(y\) phụ thuộc vào \(x\). Điều này giải thích tại sao trong hình bên trái có 5 mô hình được xây dựng tương ứng với năm lục địa, trong khi trong hình bên phải chỉ có một mô hình duy nhất được xây dựng cho tất cả các quốc gia trên thế giới.
Vậy khi nào bạn nên khai báo ánh xạ thẩm mỹ trong hàm ggplot() và khi nào bạn nên khai báo ánh xạ thẩm mỹ bên trong hàm geom_*()? Câu trả lời là nếu đa số các lớp bạn đọc sử dụng chung một dữ liệu và chung các ánh xạ thẩm mỹ, bạn nên khai báo ánh xạ thẩm mỹ bên trong hàm ggplot(). Còn trong trường hợp đa số các lớp sử dụng sữ liệu khác nhau, hoặc ánh xạ thẩm mỹ khác nhau, bạn hãy khai báo ánh xạ thẩm mỹ bên trong mỗi hàm geom_*(). Trong trường hợp bạn dùng một hàm geom_()* và không muốn sử dụng ánh xạ thẩm mỹ đã khai báo trong ggplot(), bạn có thể khai báo lại hoặc khai báo thuộc tính thẩm mỹ đó bằng NULL.
## Hình bên trái
p1<-dat%>%ggplot(aes(x = life_expectancy, y = gdp_per_capita,
color = continent)) +
geom_point(alpha = 0.4)+
geom_smooth(aes(color=NULL), se = FALSE)
## Hình ở giữa
p2<-dat%>%ggplot(aes(x = life_expectancy, y = gdp_per_capita,
color = continent)) +
geom_point(alpha = 0.4)+
geom_smooth(color="black", se = FALSE)
## Hình bên phải
p3<-dat%>%ggplot(aes(x = life_expectancy, y = gdp_per_capita,
color = continent)) +
geom_point(alpha = 0.4)+
geom_smooth(aes(color="black") , se = FALSE)
## Vẽ p1, p2, p3 trên cùng một đồ thị
grid.arrange(p1,p2,p3, nrow= 1 , ncol = 3)
Có hai cách để bạn tác động đến các thuộc tính thẩm mỹ của đồ thị trong các hàm geom_*(), đó là dùng ánh xạ thẩm mỹ (mapping) và cài đặt tham số (setting). Khác nhau giữa hai cách này là việc bạn khai báo giá trị của thuộc tính thẩm mỹ bên trong hay bên ngoài hàm aes(). Hãy quan sát đồ thị ở trên:
Hình bên trái cho đường cong của mô hình \(loess\) có màu xanh da trời. Khi gọi hàm
geom_smooth(), chúng ta đã cho thuộc tính thẩm mỹ \(color\) về giá trị mặc định bằng cách ánh xạ thuộc tính này tới giá trị \(NULL\). Màu xanh da trời là màu mặc định của các đường cong được tạo ra từgeom_smooth().Hình ở giữa chúng ta cài đặt (setting) cấu phần thẩm mỹ \(color\) bằng một giá trị cố định là “black” (màu đen). Do đó đường cong được tạo từ
geom_smooth()sẽ có màu đen giống như cài đặt. Bạn đọc chỉ cần sử dụng giá trị màu sắc có ý nghĩa với R để cài đặt cho thuộc tính thẩm mỹ \(color\). Nếu trong một hàmgeom_*()vừa có ánh xạ thẩm mỹ được khai báo trong hàmaes()và vừa có cài đặt thuộc tính thẩm mỹ (ngoài hàmaes()), \(ggplot2\) sẽ ưu tiên giá trị nằm ngoàiaes().Hình bên tay phải phức tạp hơn một chút. Khác với hình ở giữa, thuộc tính \(color\) được gán cho giá trị “black” bên trong hàm
aes(). Bạn đọc có thể thấy rằng đường cong được tạo từ hàmgeom_smooth()không có màu đen như hình ở giữa. Khi khai báo thuộc tính trong hàmaes(), chúng ta đã ánh xạ thuộc tính \(color\) của hàmgeom_smooth()đến một giá trị kiểu ký tự là “black” chứ không phải cho màu sắc của đường cong nhận giá trị màu tương ứng! Trong hàmgeom_point()trước đó đã ánh xạ biến \(continent\) tới thuộc tính thẩm mỹ \(color\), khi chúng ta tiếp tục ánh xạ một biến “black” tới \(color\) tronggeom_smooth()thì \(ggplot2\) sẽ hiểu rằng có thêm một giá trị mới cho thuộc tính \(color\) (“black”) thêm vào các giá trị hiện có (tên của 5 châu lục). Điều này giải thích tại sao trong chú giải (legend) của hình bên tay phải có 6 loại thay vì 5 loại như hình ở giữa. Đường cong tạo bởigeom_smooth()có màu xanh lá cây vì giá trị “black” có thứ hạng là 4 khi sắp xếp 6 giá trị ánh xạ tới thuộc tính \(color\) theo thứ tự tăng dần.
Khi cân nhắc sử dụng ánh xạ hay thiết lập giá trị cho các thuộc tính thẩm mỹ, bạn đọc nên cân nhắc về việc có muốn tác động lên thuộc tính thẩm mỹ nữa hay không. Nếu bạn muốn cố định giá trị cho thuộc tính thẩm mỹ, hãy sử dụng thiết lập giá trị. Còn nếu bạn muốn tác động ngược lại lên thuộc tính thẩm mỹ đó, hãy sử dụng ánh xạ thay vì thiết lập giá trị.
Hãy nói một chút về cách chú giải ghi nhận giá trị mới của một thuộc tính thẩm mỹ. Trong hình bên phải, khi chúng ta khai báo giá trị “black” cho thuộc tính \(color\), \(ggplot2\) ghi nhận “black” như một giá trị mới tương đương với tên các Châu lục đã sử dụng trong khai báo trước đó. Cách ghi nhận tên biến mới trong chú giải sẽ rất hữu ích khi chúng ta muốn tạo một đồ thị nhiều lớp và đặt tên cho từng lớp trong phần chú giải của đồ thị. Đồ thị dưới đây so sánh ba phương pháp xây dựng mô hình trên dữ liệu là phương pháp hồi quy tuyến tính thông thường (method = "lm"); hồi quy loess (method = "loess"), và mô hình cộng tính tổng quát (method = "gam")
# So sánh ba phương pháp xây dựng mô hình khác nhau của hàm geom_smooth
dat%>%ggplot(aes(x = life_expectancy, y = gdp_per_capita)) +
#Layer 1: đồ thị rải điểm
geom_point(alpha = 0.4)+
# Layer 2: Đường hồi quy tuyến tính
geom_smooth(aes(color="Hồi quy tuyến tính"), method = "lm" , se = FALSE)+
# Layer 3: Đường hồi quy loess
geom_smooth(aes(color="Hồi quy loess"), method = "loess" , se = FALSE)+
# Layer 4: Mô hình GAM (generalized additive model)
geom_smooth(aes(color="Mô hình cộng tính tổng quát"), method = "gam" , se = FALSE)
8.3.2 Các hàm geom_*() cơ bản
Các hình dạng đồ họa, gọi tắt là các \(geoms\), là một cách phổ biến để hiển thị một lớp của một đồ thị mô tả dữ liệ. Ví dụ như sử dụng geom_point() sẽ tạo ra một đồ thị phân tán, trong khi sử dụng geom_line() sẽ tạo ra các đồ thị theo đường. Danh sách các \(geoms\) và các thuộc tính thẩm mỹ bạn đọc có thể tìm thấy trong CHEAT SHEET ở trên. Ở đây chúng tôi chỉ tổng hợp và phân loại lại các \(geoms\) một cách ngắn gọn.
- Khi mô tả một biến:
- Mô tả biến rời rạc:
-
geom_bar(): hiển thị phân phối của biến rời rạc dưới dạng các thanh.
-
- Mô tả một biến liên tục:
-
geom_histogram(): nhóm dữ liệu liên tục lại vào các nhóm (bin) và hiển thị dưới dạng các thanh. -
geom_density(): vẽ đường mô tả hàm mật độ xác suất của biến ngẫu nhiên liên tục. Hàm mật độ xác suất được ước lượng bằng phương pháp kernel. Giá trị hàm mật độ tại một điểm \(x\) bất kỳ được tính bằng trung bình giá trị hàm \(K\), được gọi là hàm \(kernel\), tính trên khoảng cách từ điểm \(x\) tới tất cả các quan sát. Nếu \(\hat{f}(x)\) là giá trị hàm mật độ tính tại \(x\) bằng phương pháp kernel thì ta có
-
- Mô tả biến rời rạc:
\[\begin{align}
\hat{f}(x) = \cfrac{1}{nh} \times \sum\limits_{i = 1}^{n} \ K\left( \cfrac{x - x_i}{h} \right)
\end{align}\]
trong đó \(x_i\) là giá trị quan sát thứ \(i\) và \(h\) là được gọi là tham số làm mịn. \(h\) càng lớn thì hàm \(\hat{f}\) sẽ càng mịn. Hàm \(K\) được sử dụng làm hàm kernel mặc định cho geom_density() là hàm mật độ của biến ngẫu nhiên phân phối chuẩn.
-
geom_boxplot(): vẽ đồ thị boxplot của một biến liên tục.
p1<-gapminder%>%filter(year==2011)%>%ggplot(aes(fertility))+
geom_histogram()
p2<-gapminder%>%filter(year==2011)%>%ggplot(aes(fertility))+
geom_density()
p3<-gapminder%>%filter(year==2011)%>%ggplot(aes(y=fertility))+
geom_boxplot()
grid.arrange(p1,p2,p3,nrow=1,ncol=3)
- Khi mô tả hai biến:
- Cả hai biến đều là liên tục:
-
geom_point(): đồ thị rải điểm là cách hiệu quả nhất để mô tả hai biến liên tục. Bạn đọc có thể sử dụng cùng vớigeom_smooth()để mô tả mối quan hệ giữa hai biến. Trong trường hợp biến liên tục nhưng các điểm bị trùng nhau khi hiển thị, bạn đọc có thể sử dụnggeom_jitter()thay thế chogeom_point()để hiện thị tốt hơn.geom_jitter()sẽ di chuyển các điểm một cách ngẫu nhiên xung quanh điểm ban đầu để tránh việc hiển thị điểm bị trùng nhau. -
geom_line(): thường để mô tả hai biến liên tục mà một trong hai biến là kiểu thời gian. -
geom_text(): tương tựgeom_point()nhưng hiển thị biến kiểu ký tự thay vì hiển thị điểm.geom_text()thường sử dụng kết hợp vớigeom_point(). Lưu ý để khi hiển thị biến kiểu ký tự và điểm không bị trùng nhau là bạn đọc cần phải sử dụng thêm các tham số như \(hjust\), \(vjust\) để điều chỉnh trong hàmgeom_text(). Trong nhiều trường hợp, để hiện thị biến kiểu ký tự tốt hơn, chúng tôi sử dụng hàmgeom_text_repel()từ thư viện bổ sung \(ggrepel\). -
geom_label(): tương tựgeom_text(). Hàm có thể hiện thị tốt hơn trong thư viện \(ggrepel\) là hàmgeom_label_repel(). Bạn đọc quan sát ví dụ dưới đây để thấy các hàmgeom_text_repel()vàgeom_label_repel()cho kết quả tốt hơngeom_text()vàgeom_label()
-
- Cả hai biến đều là liên tục:
p1<-gapminder%>%filter(year==2011, region=="South-Eastern Asia")%>%
ggplot(aes(fertility, infant_mortality))+
geom_point()+geom_text(aes(label = country), vjust=1.1)+
ggtitle("Sử dụng geom_text()")
p2<-gapminder%>%filter(year==2011, region=="South-Eastern Asia")%>%
ggplot(aes(fertility, infant_mortality))+
geom_point()+geom_label(aes(label = country), vjust=0.9)+
ggtitle("Sử dụng geom_label()")
grid.arrange(p1,p2,nrow=1,ncol=2)
p1<-gapminder%>%filter(year==2011, region=="South-Eastern Asia")%>%
ggplot(aes(fertility, infant_mortality))+
geom_point()+geom_text_repel(aes(label = country), vjust=1.1)+
ggtitle("Sử dụng geom_text_repel()")
p2<-gapminder%>%filter(year==2011, region=="South-Eastern Asia")%>%
ggplot(aes(fertility, infant_mortality))+
geom_point()+geom_label_repel(aes(label = country), vjust=0.9)+
ggtitle("Sử dụng geom_label_repel()")
grid.arrange(p1,p2,nrow=1,ncol=2)
-geom_bin2d() và geom_density2d() tương tự như geom_histogram() và geom_density() dùng để mô tả phân phối của hai biến liên tục. Khi số lượng quan sát cần hiển thị lớn thì sử dụng đồ thị rải điểm sẽ không hiệu quả. geom_bin2d() chia miền giá trị của từng biến thành các khoảng bằng nhau và đếm trong mỗi hình chữ nhật có bao nhiêu điểm sau đó sử dụng màu sắc từ đậm đến nhạt để mô tả số lượng điểm trong từng hình chữ nhật từ nhỏ đến lớn. geom_density2d() sử dụng phương pháp kernel để tính giá trị hàm mật độ trong không gian hai chiều. Khoảng cách từ quan sát \(x_i\) đến điểm \(x\) được sử dụng là khoảng cách Euclid. Kernel được sử dụng là hàm mật độ của biến ngẫu nhiên phân phối chuẩn hai chiều. geom_density2d() vẽ các đường nỗi các điểm có giá trị hàm mật độ bằng nhau. Hình vẽ dưới đây mô tả phân phối đồng của hai biến \(favorite\_count\) và \(retweet\_count\) trong dữ liệu \(trump\_tweet\).
# geom_bind2d
p1<-trump_tweets%>%mutate(log_favorite_count = log(favorite_count),
log_retweet_count = log(retweet_count)) %>%
ggplot(aes(log_favorite_count,log_retweet_count))+geom_bin2d()+theme_minimal()
# geom_density2d
p2<-trump_tweets%>%mutate(log_favorite_count = log(favorite_count),
log_retweet_count = log(retweet_count)) %>%
ggplot(aes(log_favorite_count,log_retweet_count))+geom_density2d()+theme_minimal()
grid.arrange(p1,p2,nrow=1, ncol = 2)
- Một biến liên tục và một biến rời rạc
-
geom_boxplot()vẽ đồ thị boxplot của một biến liên tục theo các nhóm được phân loại theo biến rời rạc. -
geom_violin()tương tựgeom_boxplot()và sử dụng cùng vớigeom_boxplot()để bổ sung thông tin khi mô tả phân phối của biến liên tục trong từng nhóm. Chúng ta có thể mô tả phân phối của biến \(life_expectancy\) trong các năm 1990, 2000, và 2010 trong dữ liệu \(gapminder\) như sau
-
p1<- gapminder%>%filter(year %in% c(1990,2000,2010))%>%ggplot(aes(x = as.factor(year),y = life_expectancy))+geom_boxplot()
p2<-gapminder%>%filter(year %in% c(1990,2000,2010))%>%ggplot(aes(x = as.factor(year),y = life_expectancy))+geom_violin()
grid.arrange(p1,p2,nrow=1, ncol = 2)
- Mô tả hai biến rời rạc:
-
geom_count()được sử dụng để mô tả hai biến rời rạc.geom_count()tạo ra đồ thị thường được gọi là đồ thị kiểu bong bóng, mà kích thước của mỗi bong bóng cho biết số lượng hay mật độ của các điểm rời rạc. Ví dụ như khi mô tả hai biến là \(cut\) và \(color\) trong dữ liệu \(diamonds\), chúng ta sử dụnggeom_count()như sau
-
diamonds%>%ggplot(aes(cut,color))+geom_count(color="lightskyblue")+theme_minimal()
Không khó để nhận ra tỷ trọng lớn các viên kim cương có biến \(cut\) nhận giá trị \(ideal\), và trong các viên kim cương đó màu \(G\) có tỷ trọng lớn nhất.
- Mô tả ba biến: Sử dụng các hình ảnh kiểu 3 chiều không phải là một phương pháp tốt để hiển thị ba biến trên cùng một đồ thị. \(ggplot2\) thường hiển thị dữ liệu trên một mặt phẳng và sử dụng một thuộc tính thẩm mỹ nào đó làm chiều thứ ba.
geom_title()thường được sử dụng để mô tả ba biến cùng một lúc bằng cách sử dụng trục \(x\) và \(y\) đề mô tả hai biến và màu sắc để mô tả biến thứ ba. Hình vẽ dưới đây sử dụnggeom_tile()để mô tả ba biến \(region\), \(year\), và \(life\_expectancy\). \(life\_expectancy\) được tính theo trung bình của vùng qua các năm.
danhsach<-gapminder%>%filter(year==2010)%>%
group_by(region)%>%summarise(continent = unique(continent))%>%
arrange(continent)
gapminder%>%group_by(year,continent, region)%>%summarise(life_expectancy = mean(life_expectancy,na.rm = TRUE))%>%
ggplot()+geom_tile(aes(x = year, y = region , fill = life_expectancy), color= "grey")+
scale_fill_gradientn(colours = c(rgb(0.8,0.3,0.3),rgb(0.9,0.9,0.9),rgb(0.3,0.3,0.9)))+
scale_x_continuous(breaks = seq(1960,2010,5))+
scale_y_discrete(limits = danhsach$region)+
theme_minimal()
Trong đồ thị ở trên chúng tôi đã sử dụng thêm các hàm scale_*() để kiểm soát ánh xạ thẩm mỹ. Chẳng hạn giá trị năm (trục x) sẽ là cách đều 5 năm, các vùng trên trục \(y\) được sắp xếp theo lục địa (Châu Đại Dương, Châu Âu, Châu Á, Châu Mỹ, và cuối cùng là Châu Phi). Dải màu sắc cũng được cho giá trị dải màu từ đỏ sang xanh da trời. Chúng ta sẽ thảo luận về scale_*() trong phần sau của chương sách.
8.3.3 Các hàm stat_*()
Bạn đọc cũng có thể xây dựng các lớp cho đồ thị \(ggplot2\) bằng các hàm stat_*(). Các hàm số này thường không hiển thị dữ liệu ở trạng thái ban đầu mà thường hiển thị dữ liệu dưới một phép biến đổi thống kê hoặc một sau một tóm tắt dữ liệu theo một cách nào đó. Nhiều hàm stat_*() được gọi thông qua các hàm geom_*(), chẳng hạn như stat_bin() tương đương với geom_histogram() và geom_bar(); stat_smooth() tương đương với geom_smooth(); … Sự tương đồng hay khác biệt giữa sử dụng hàm stat_*() và hàm geom_*() sẽ được thảo luận ở phần cuối của chương sách.
Thay vì sử dụng geom_*(), chúng ta có thể sử dụng stat_*() để mô tả phân phối của các biến liên tục
# ĐỒ thị histogram
p1<-gapminder%>%filter(year==2011)%>%ggplot(aes(fertility))+
stat_bin()+ggtitle("Đồ thị histogram của fertility")
# hàm phân phối xác suất
p2<-gapminder%>%filter(year==2011)%>%ggplot(aes(fertility))+
stat_density()+ggtitle("Hàm mật độ")
p3<-gapminder%>%filter(year==2011)%>%ggplot(aes(fertility))+
stat_boxplot()+ggtitle("Đồ thị boxplot")
grid.arrange(p1,p2,p3,nrow=1,ncol=3)
8.4 Scale
\(Scale\) trong ggplot2 kiểm soát ánh xạ từ dữ liệu đến thuộc tính thẩm mỹ của đồ thị. Các hàm scale_*() lấy dữ liệu ban đầu và biến đổi thành các đối tượng trực quan mà bạn có thể nhìn thấy, như kích thước, màu sắc, vị trí hoặc hình dạng. Bạn có thể tạo biểu đồ bằng \(ggplot2\) mà không cần biết chính xác ánh xạ hoạt động như thế nào, nhưng hiểu về \(scale\) và học cách thao tác các hàm scale_*() sẽ giúp bạn kiểm soát tốt hơn rất nhiều.
8.4.1 Vị trí xuất hiện trên trục tọa độ
Đa số các đồ thị trong \(ggplot2\) hiển thị dữ liệu trên trục tọa độ Descartes nên chúng tôi sẽ tập trung vào cách dữ liệu hiển thị trên hai trục tọa độ \(x\) và \(y\). Khi ánh xạ các cột dữ liệu tới các trục tọa độ \(x\) và \(y\) nếu chúng ta không sử dụng \(scale\), các điểm sẽ được hiển thị đúng như giá trị của điểm đó trên các trục tọa độ. Trong nhiều trường hợp, hiển thị tại vị trí đúng như dữ liệu ban đầu sẽ không mang lại hiệu quả. Ví dụ như khi mô tả hai biến \(total\) và \(population\) của dữ liệu \(murders\), bạn đọc có thể so sánh cách hiển thị giữa việc không kiểm soát (hình bên trái) và có kiểm soát ánh xạ thẩm mỹ như hình dưới đây
p1<-murders%>%ggplot(aes(x = population,y = total))+geom_point(size = 3, color = "lightskyblue")+
theme_minimal()+ggtitle("Không sử dụng scale")
p2<-murders%>%ggplot(aes(x = population,y = total))+geom_point(size = 3, color = "lightskyblue")+
theme_minimal()+
scale_x_continuous(trans = "log10")+
scale_y_continuous(trans = "log10")+
ggtitle("Có sử dụng scale (log10)")
grid.arrange(p1,p2,nrow=1,ncol=2)
Có thể thấy rằng hình bên phải hiển thị rõ ràng hơn hình bên trái sau khi chúng ta kiểm soát ánh xạ từ biến \(population\) đến thuộc tính \(x\) và từ biến \(total\) đến thuộc tính \(y\) bằng các hàm scale_x_continuous() và scale_y_continuous(). Đây là hai hàm số được dùng để kiểm soát vị trí xuất hiện của các điểm trên trục tọa độ khi các biến trong ánh xạ là các biến kiểu số liên tục. Các tham số có thể được sử dụng trong các hàm này bao gồm có:
Tham số \(trans\), là viết tắt của transformation, nhận giá trị mặc định là ‘identity’ nghĩa là lấy chính xác giá trị của biến ánh xạ vào thuộc tính \(x\) hoặc \(y\) tương ứng. Để biết các giá trị mà tham số này có thể nhận được bạn đọc có thể tham khảo trong tài liệu đi kèm với hàm
scale_x_continuous()vàscale_y_continuous(). Khi scale các biến, chẳng hạn như \(x_1\) và \(x_2\), là các biến ánh xạ tới \(x\) và \(y\) trong ánh xạ thẩm mỹ bằng một hàm \(f\) được khai báo bằng tham số \(trans\), giá trị xuất hiện trên trục tọa độ \(x\) và \(y\) sẽ tương ứng là \(f(x_1)\) và \(f(x_2)\). Chẳng hạn như trong đồ thị phía bên phải ở trên, khi chúng ta thực hiện scale sử dụng hàm \(log10\), tọa độ của các điểm (các quốc gia) sẽ là \(log10(population)\) và \(log10(total)\). Việc chuyển đổi này sẽ hữu ích bởi rất đa số các quốc gia có dân số nhỏ, trong khi có một vài quốc gia có dân số rất lớn. Thực hiện chuyển đổi dữ liệu bằng các hàm \(log\) sẽ giúp cho khoảng cách của các điểm cách đều nhau hơn và dễ dàng phân biệt hơn.Tham số \(limits\) giới hạn giá trị trên các trục \(x\) và \(y\). Mỗi khi chúng ta vẽ đồ thị sử dụng \(ggplot2\), tham số \(limits\) mặc định sẽ đảm bảo việc hiển thị được đầy đủ nhất. Tuy nhiên, khi chúng ta muốn so sánh hai dữ liệu con trên cùng một miền giá trị của \(x\) và \(y\), sử dụng tham số \(limits\) cho phép so sánh rõ ràng hơn là sử dụng tham số mặc định. Hình vẽ dưới đây mô tả hai biến \(fertility\) và \(life\_expectancy\) trong năm 1960 và 2010 và không sử dụng scale.

Không thể thấy rõ được sự khác biệt giữa hai năm 1960 và 2010 nếu không biểu diễn các biến trên cùng một miền giá trị của \(x\) và \(y\). Hình phía dưới sử dụng tùy biến \(limits\). Để khai báo tham số cho tùy biến này chúng ta sử dụng một véc-tơ hai chiều chứa giá trị nhỏ nhất và giá trị lớn nhất trên trục mà bạn muốn hiển thị. Rõ ràng đã có sử khác nhau đáng kể về sự phân bố của các điểm trong năm 1960 và năm 2010.
p1<-gapminder%>%filter(year==1960)%>%
ggplot(aes(fertility,life_expectancy))+
geom_point(shape = 21, size = 3,alpha = 0.8, fill= "lightskyblue")+
ggtitle("Năm 1960")+
scale_x_continuous(limits = c(1,9))+
scale_y_continuous(limits = c(25,85))
p2<-gapminder%>%filter(year==2010)%>%
ggplot(aes(fertility,life_expectancy))+
geom_point(shape = 21, size = 3,alpha = 0.8, fill= "lightskyblue")+
ggtitle("Năm 2010")+
scale_x_continuous(limits = c(1,9))+
scale_y_continuous(limits = c(25,85))
grid.arrange(p1,p2,nrow=1,ncol=2)
- Tham số \(breaks\) kiểm soát vị trí các điểm đánh dấu xuất hiện trên các trục \(x\) và trục \(y\). Chúng tôi thường kết hợp \(breaks\) với tham số \(labels\) để kiểm soát đồng thời vị trí và cách hiển thị trên các trục số. Ví dụ như trong hình so sánh đồ thị rải điểm của năm 1960 và 2010 chúng ta muốn giá trị xuất hiện trên các trục \(x\) là các số 2, 4, 6, 8 thay vì 2,5; 5,0; và 7, và các số trên trục \(y\) xuất hiện tại các vị trí 10, 30, 50, 70, và 90 thay vì 40, 60, 80 như hiện tại, chúng ta chỉ cần gán giá trị tham số \(breaks\) bằng véc-tơ chứa các giá trị mà chúng ta muốn hiển thị. Lưu ý rằng \(breaks\) có chữ \(s\) ở cuối để phân biệt với từ khóa \(break\).
p1<-gapminder%>%filter(year==1960)%>%
ggplot(aes(fertility,life_expectancy))+
geom_point(shape = 21, size = 3,alpha = 0.8, fill= "lightskyblue")+
ggtitle("Năm 1960")+
scale_x_continuous(limits = c(1,9),
breaks = c(2,4,6,8),
labels = paste(c(2,4,6,8),"trẻ em"))+
scale_y_continuous(limits = c(25,85),
breaks = c(10,30,50,70,90),
labels = paste(c(10,30,50,70,90),"tuổi"))
p2<-gapminder%>%filter(year==2010)%>%
ggplot(aes(fertility,life_expectancy))+
geom_point(shape = 21, size = 3,alpha = 0.8, fill= "lightskyblue")+
ggtitle("Năm 2010")+
scale_x_continuous(limits = c(1,9),
breaks = c(2,4,6,8),
labels = paste(c(2,4,6,8),"trẻ em"))+
scale_y_continuous(limits = c(25,85),
breaks = c(10,30,50,70,90),
labels = paste(c(10,30,50,70,90),"tuổi"))
grid.arrange(p1,p2,nrow=1,ncol=2)
Khi một trong hai biến liên tục là biến kiểu thời gian thì hàm số sử dụng để kiểm soát giá trị hiển thị là scale_x_date() với hai tham số thường được sử dụng là \(date\_break\) và \(date\_labels\). Bạn đọc quan sát dữ liệu \(AirPassengers\) được trực quan hóa khi sử dụng scale_x_date() như sau
dat<-data.frame(Number_Passengers = AirPassengers,
Month = seq(as.Date("1949-01-01"), by = "month", length.out = 144))
p1<-dat%>%ggplot(aes(x = Month, y = Number_Passengers))+
geom_line() + ggtitle("Không sử dụng scale")
p2<-dat%>%ggplot(aes(x = Month, y = Number_Passengers))+
geom_line()+ ggtitle("Sử dụng scale_x_date()")+
scale_x_date(date_break = "2 years", date_labels = "%b\n%Y" )+
scale_y_continuous(breaks = seq(100,600,length=6))
grid.arrange(p1,p2,nrow=1,ncol=2)
Khi giá trị trên trục \(x\) hoặc trục \(y\) là các giá trị rời rạc, các hàm số sử dụng để kiểm soát ánh xạ thẩm mỹ từ biến đến các trục tọa độc là scale_x_discrete() và scale_y_discrete(). Các tham số thường sử dụng bao gồm \(limits\) và \(labels\). Tham số \(limits\) được sử dụng để cho biết các giá trị nào của biến rời rạc xuất hiện trên đồ thị, trong khi tham số \(labels\) cho biết từng giá trị của biến rời rạc xuất hiện như thế nào
p1<-murders%>%mutate(rate = total/population*10^6)%>%
ggplot(aes(region,rate))+
geom_boxplot()+ggtitle("Không sử dụng scale")
# Sử dụng tham số limits cho giá trị trên trục x
p2<-murders%>%mutate(rate = total/population*10^6)%>%
ggplot(aes(region,rate))+
geom_boxplot()+ggtitle("Không sử dụng scale")+
scale_y_continuous(limits = c(0,50))+
# chỉ hiển thị boxplot cho 2 vùng "Northeast" và "West"
scale_x_discrete(limits = c("Northeast", "West"))+
ggtitle("Sử dụng dụng tham số limits")
# Sử dụng tham số labels cho giá trị trên trục x
p3<-murders%>%mutate(rate = total/population*10^6)%>%
ggplot(aes(region,rate))+
geom_boxplot()+ggtitle("Không sử dụng scale")+
scale_y_continuous(limits = c(0,100))+
# Thay thế giá trị hiển thị trên trục số bằng labels
scale_x_discrete(labels = c("Northeast" = "Đông Bắc",
"West" = "Miền Tây",
"South" = "Miền Nam",
"North Central" = "Miền Bắc"))+
ggtitle("Sử dụng dụng tham số labels")
grid.arrange(p1,p2,p3,nrow=1,ncol=3)
8.4.2 Màu sắc hiển thị và chú giải
Thuộc tính thẩm mỹ được sử dụng phổ biến nhất là màu sắc. Có nhiều cách để ánh xạ giá trị của biến tới màu sắc trong \(ggplot2\). Vì màu sắc là một chủ đề phức tạp nên phần này chúng tôi sẽ bắt đầu bằng việc thảo luận một chút về lý thuyết màu sắc. Sau đó chúng tôi sẽ giới thiệu đến bạn đọc về thang màu liên tục, thang màu rời rạc và thang màu tổng hợp. Chúng tôi cũng sẽ đề cập đến các thang màu dành cho biến kiểu thời gian ngày/giờ, độ trong của các màu sắc hiển thị và nguyên tắc chú giải được thiết lập trong các đồ thị \(ggplot2\).
8.4.2.1 Cảm nhận về màu sắc
Trong vật lý, màu sắc được tạo ra bởi hỗn hợp các bước sóng ánh sáng. Để mô tả đầy đủ về một màu sắc, chúng ta cần biết sự kết hợp chính xác của các bước sóng. Sự thật thì mắt con người chỉ có ba cơ quan cảm nhận màu sắc khác nhau, và vì vậy chúng ta có thể tóm tắt khả năng cảm nhận về bất kỳ màu nào chỉ bằng ba con số. Một không gian màu có thể quen thuộc với bạn đọc là không gian màu RGB, không gian mà mọi màu sắc được xác định theo cường độ ánh sáng đỏ, xanh da trời và xanh lá cây để tạo ra màu đó. Ưu điểm của không gian màu này là sự đơn giản do mỗi màu sắc đều được mô tả bằng ba con số từ 0 đến 255 hoàn toàn độc lập với nhau. Một vấn đề với không gian này là các dải màu liên tục nhận được bằng cách tăng giảm các cường độ màu đỏ, xanh lam, xanh lá cây lại không giống như cách nhận thức về màu sắc của còn người. Khi nhìn vào một màu cụ thể, chúng ta không thể ước tính được cường độ mỗi màu là bao nhiêu, điều này có thể gây khó khăn cho việc tạo ánh xạ từ một biến liên tục sang một dải màu.
Mỗi khi hiển thị một giá trị màu sắc trong không gian RGB, R thường sử dụng ký tự có 6 chữ số trong hệ 16 (từ 0 đến F) và bắt đầu bằng một dấu ‘#’ thay vì một véc-tơ ba chiều đại diện cho 3 sắc đỏ, xanh lá cây, và xanh lam. Hai chữ số đầu đại diện cho sắc đỏ, 2 chữ số tiếp theo đại diện cho màu xanh lá cây và 2 chữ số cuối đại diện cho màu lam. Chẳng hạn như “#FF0000” sẽ là màu đỏ, “#00FF00” là màu xanh lá cây và “#0000FF” là màu xanh lam.
Một không gian màu được chuyển đổi từ không gian RGB là không gian Lab trong đó L đại diện cho độ tương phản sáng-tối của màu sắc, trục tọa độ a và b cho biết các vị trí của màu trên trên trục đỏ đến xanh lam và vàng đến xanh lá. Cải tiến từ không gian RGB sang không gian Lab giúp cho các dải màu sắc tương ứng hơn với cách khả năng nhận biết màu sắc của con người, tuy nhiên vẫn còn khoảng cách giữa không gian Lab với nhận thức màu sắc. Không gian \(Lab\) cũng có các ưu điểm riêng, do đó \(ggplot2\) mặc định sử dụng không gian \(Lab\) khi nội suy tuyến tính các màu sắc nằm giữa hai màu bất kỳ khi chúng ta ánh xạ một biến liên tục lên thuộc tính thẩm mỹ màu sắc.
Một không gian màu khác có thể hạn chế vấn đề của không gian RGB là không gian màu HCL với ba thành phần màu: màu sắc (Hue), sắc độ (Chroma) và độ chói (Luminance):
Màu sắc nằm trong khoảng từ 0 đến 360 (một góc) và cho biết màu muốn hiển thị.
Sắc độ là “độ tinh khiết” của một màu, nằm trong khoảng từ 0 (xám) đến mức tối đa thay đổi theo độ sáng.
Độ sáng là độ sáng của màu, dao động từ 0 (đen) đến 1 (trắng).
Ba chiều có những đặc tính khác nhau. Tương tự như không gian màu Lab, màu sắc trong HCL được sắp xếp xung quanh một hình tròn và không được coi là có trật tự; ví dụ: màu xanh lá dường như không lớn hơn hay nhỏ hơn màu đỏ và màu xanh lam dường như không lớn hơn hay nhỏ hơn màu xanh lá. Ngược lại, cả sắc độ và độ sáng đều được coi là có trật tự: màu hồng được coi là nằm giữa màu đỏ và trắng, và màu xám được coi là nằm giữa màu đen và trắng. Tạo các thang màu sắc từ không gian HCL thường được dựa trên nguyên tắc cố định 2 tham số và thay đổi tham số còn lại. Do không gian màu HCL gần với nhận thức màu sắc của con người hơn nên các dải màu được tạo ra sẽ “cách đều” nhau hơn theo cách mà chúng ta nhận thức.
Xin được nhắc lại rằng màu sắc là một chủ để phức tạp mà phạm vi của nó vượt rất xa những gì mà chúng tôi đề cập ở trên. Bạn đọc nên tham khảo thêm các tài liệu chuyên ngành khoa học máy tính để có thể sử dụng màu sắc một cách hiệu quả nhất.
8.4.2.2 Dải màu liên tục
Dải màu liên tục được sử dụng để hiển thị giá trị của một biến liên tục trên bề mặt phẳng. Để kiểm soát màu sắc trong \(ggplot2\), chúng ta sử dụng các hàm scale_color_*(). Lưu ý rằng các thuộc tính thẩm mỹ \(color\) và \(fill\) là tương đồng nhau, do đó bất kỳ hàm scale_color_*() cũng có hàm scale_fill_*() tương ứng.
Dải màu liên tục thường được sử dụng cùng với các \(geoms\) có hình dạng đồ họa cần màu sắc để phân biệt trên trên mặt phẳng như geom_polygon(), geom_tile() (hoặc geom_raster()), và geom_bin2d(). Mỗi khi chúng ta cho một biến liên tục ánh xạ đến thuộc tính thẩm mỹ màu sắc, \(ggplot2\) sẽ tự động hiểu rằng chúng ta sử dụng dải màu liên tục để mô tả biến đó. Hình vẽ dưới đây mô tả hàm mật độ của biến ngẫu nhiên phân phối chuẩn hai chiều trung bình 0, phương sai 1 và hệ số tương quan \(\rho = 0.8\). Lưu ý rằng hàm mật độ của hai biến ngẫu nhiên phân phối chuẩn \(\mathcal{N}(0,1)\) với hệ số tương quan \(\rho = 0.8\) được tính như sau
\[\begin{align}
f(x,y) = \cfrac{1}{2 \pi \sqrt{1-\rho^2}} \ \exp \left(- \cfrac{x^2 + y^2 - 2\rho x y}{1-\rho^2} \right)
\end{align}\]
# tạo lưới điểm trên hình vuông [-2,2] * [2-,2]
n<-100
x<-rep(1:n,n)/n*4-2
y<-sort(x, decreasing = FALSE)
rho<-0.8
dat<-data.frame(x=x,y=y,dens = 1/(2*pi*sqrt(1-rho^2)) * exp(-(x^2+y^2-2*rho*x*y)/(1-rho^2)))
p<-dat%>%ggplot(aes(x,y,fill=dens))+geom_raster()+theme_minimal()Phương pháp đơn giản nhất để kiểm soát ánh xạ thẩm mỹ từ một biến liên tục đến màu sắc là lựa chọn các dải màu liên tục có sẵn trong \(ggplot2\) hoặc trong các thư viện bổ sung. Các dải màu có sẵn này đều được xây dựng để những người gặp khó khăn trong phân biệt màu sắc cũng có thể cảm nhận được. Trong hình vẽ dưới đây chúng tôi lựa chọn các dải màu: 1. Dải màu mặc định của \(ggplot2\), 2. Dải màu \(viridis\), 3. Dải màu \(distiller\) và 4. Dải màu \(fermenter\). Mỗi dải màu sẽ có tùy biến \(palette\) để lựa chọn.
# tạo lưới điểm trên hình vuông [-2,2] * [2-,2]
p1<-p + scale_fill_continuous()+
ggtitle("Màu mặc định") # sử dụng dải màu mặc định
p2<-p + scale_fill_viridis_c()+ # Dải màu viridis liên tục
ggtitle("Dải màu viridis")
p3<-p + scale_fill_distiller()+ # Dải màu distiller
ggtitle("Dải màu distiller")
p4<-p + scale_fill_fermenter()+ # Dải màu fermenter
ggtitle("Dải màu fermenter")
grid.arrange(p1,p2,p3,p4,nrow=2,ncol=2)
Để dải màu sắc liên tục có tính cá nhân hóa cao hơn, bạn đọc cần chỉ định thang màu sắc thay vì sử dụng các thang màu có sẵn. \(Gradient\) \(scale\) là một công cụ mạnh mẽ giúp bạn thực hiện việc này. Bạn chỉ cần cung cấp các giá trị màu sắc tương ứng với giá trị nhỏ nhất, giá trị lớn nhất, có thể thêm một vài giá trị trung gian, \(ggplot2\) sẽ nội suy tuyến tính ra các màu sắc trong thang màu. Các hàm số có thể sử dụng để tạo thang màu bao gồm
scale_fill_gradient()tạo một thang màu liên tục giữa hai màu sắc mà bạn khai báo. Hai tham số được sử dụng để khai báo hai điểm đầu của dải màu là tham số \(low\) và tham số \(high\). Mỗi khi chúng ta ánh xạ tuyến tính từ một biến liên tục đến màu sắc, \(ggplot2\) mặc định sử dụng dải màu liên tục theo hàm số này với giá trị low là “#132B43” và giá trị high là “#56B1F7”. Không gian để nội suy tuyến tính là không gian màu Lab.scale_fill_gradient2()tạo một thang màu liên tục từ ba màu, bao gồm một màu sắc ở giữa. Ngoài hai giá trị là hai điểm đầu của hai thang màu, chúng ta cần khai báo thêm một màu ở giữa bằng tham số \(mid\) và tham số \(midpoint\) cho biết giá trị nào của biến ánh xạ tới thuộc tính màu sắc tương ứng với màu được khai báo với tham số \(mid\). Nếu không khai báo tham số \(midpoint\) sẽ nhận giá trị mặc định là 0.scale_fill_gradientn()tạo một thang màu liên tục từ một véc-tơ chứa các màu sắc khai báo.
p1<-p + ggtitle("Màu mặc định") # sử dụng dải màu mặc định
p2<-p + scale_fill_gradient(low = "blue", high = "red")+
ggtitle("Dải màu từ xanh lam đến đỏ") # sử dụng dải màu từ xanh lam đến đo
p3<-p + scale_fill_gradient2(low = "blue", high = "red", mid = "white", midpoint = 0.12)+
ggtitle("Dải màu từ xanh lam đến đỏ điểm giữa là trắng")
p4<-p + scale_fill_gradientn(colours = c("#00FF00","#FFFFFF","#0000FF", "#FFFF00"))+
ggtitle("Dải màu đi qua nhiều điểm màu")
grid.arrange(p1,p2,p3,p4,nrow=2,ncol=2)
Cả ba hàm số kể trên đều nội suy tuyến tính trong không gian màu \(Lab\) để tạo ra các giải màu liên tục. Khi nói đến nội suy tuyến tính giữa hai màu sắc, sẽ dễ hiểu nếu chúng ta sử dụng không gian RGB mà tất cả các màu đều nằm trong một hình lập phương với điểm (0,0,0) là màu đen, (1,1,1) là màu trắng… Bạn đọc có thể hiểu như sau: trong mỗi không gian mỗi màu sắc hiển thị có ba thành phần là cường độ màu đỏ (r), cường độ màu xanh lá (g) cường độ màu xanh lam (b) … Một dải màu bao gồm \(n\) màu, bắt đầu từ màu \(m_1\) bao gồm các thành phần \((r_1, g_1, b_1)\), đến màu \(m_n\) với thành phần \((r_n, g_n, b_n)\) sẽ là các màu \(m_i\) có các thành phần tương ứng \[\begin{align} r_i = \left[r_1 + (i-1) * \cfrac{r_n - r_1}{(n-1)} \right] \\ g_i = \left[g_1 + (i-1) * \cfrac{g_n - g_1}{(n-1)} \right] \\ b_i = \left[b_1 + (i-1) * \cfrac{b_n - b_1}{(n-1)} \right] \end{align}\]
Đáng tiếc là trong không gian \(Lab\) việc nội suy màu sắc không đơn giản như vậy. Việc nội suy dựa trên các tính toán phức tạp và kết quả cuối cùng là các công thức gần đúng. Ưu điểm của nội suy màu sắc trong không gian \(Lab\) so với không gian \(RGB\) sự chuyển đổi màu sắc giữa các điểm mượt mà hơn rất nhiều trong cách nhận biết màu sắc của con người.

Cả hai hình đều sử dụng dải màu liên tục từ xanh lam đến đỏ để mô tả một biến liên tục là mật độ của phân phối chuẩn hai chiều có hệ số tương quan \(\rho=0\), hình bên trái nội suy trong không gian RGB, hình bên phải nội suy trong không gian Lab. Có thể thấy rằng việc chuyển hóa màu sắc từ xanh lam sang đỏ khi sử dụng không gian màu Lab là mượt hơn nhiều so với không gian RGB.
Tương tự như vị trí trên trục tọa độ, các tham số \(limits\), \(breaks\), và \(label\) cũng có thể được sử dụng trong các hàm \(scale_fill_*()\) để kiểm soát các thang màu liên tục. Tham số \(limits\) nhận giá trị là một véc-tơ hai phần tử, phần tử thứ nhất cho biết màu sắc bắt đầu trong thang màu tương ứng với giá trị nào trong biến liên tục và phần tử thứ hai cho biết màu sắc kết thúc của thang màu ứng với giá trị nào của biến liên tục. Hàm \(breaks\) và \(labels\) sử dụng để thay đổi giá trị trên thang màu của chú giải.
# limits cho biết hai giá trị tương ứng với điểm đầu và cuối của dải màu
p1<-p + scale_fill_gradient(low = "blue", high = "red",
limits = c(0,0.5))+
ggtitle("Tham số limits")
# breaks cho biết các giá trị nào xuất hiện trên chú giải
# labels cho biết giá trị hiển thị trong chú giải
p2<-p + scale_fill_gradient(low = "blue", high = "red",
limits = c(0,0.3),
breaks = c(0.1,0.15,0.25),
labels = paste("Density at", c(0.1,0.15,0.25)))+
ggtitle("Tham số breaks và labels")
grid.arrange(p1,p2,nrow=1,ncol=2)
8.4.2.3 Dải màu rời rạc
Dải màu rời rạc dùng để mô tả thuộc tính thẩm mỹ màu sắc của các biến rời rạc. Hàm số dùng để kiểm soát màu sắc rời rạc trong \(ggplot2\) là scale_fill_discrete() và scale_color_discrete(). Mỗi khi sử dụng các hàm số kiểm soát màu sắc rời rạc, \(ggplot2\) sẽ mặc định sử dụng dải màu rời rạc “cách đều nhau” trong không gian \(HCL\). Dải màu mặc định này có cùng sắc độ (Chromes hay tham số \(c\)), độ sáng (Luminance hay tham số \(l\)) và giá trị \(h\) cách đều nhau từ góc 15 độ (\(h\) nhận giá trị từ 0 đến 360 độ). Bạn đọc muốn sử dụng các dải màu rời rạc trong không gian \(hcl\) thì có thể sử dụng scale_fill_hue() và scale_color_hue() thay vì scale_fill_discrete() và scale_color_discrete().
p1<-gapminder%>%filter(year==2011)%>%
ggplot(aes(continent,fill=continent))+geom_bar()
p2<-gapminder%>%filter(year==2011)%>%
ggplot(aes(continent,fill=continent))+geom_bar()+
scale_fill_hue(h=c(0,360)+15+360/5)
p3<-gapminder%>%filter(year==2011)%>%
ggplot(aes(continent,fill=continent))+geom_bar()+
scale_fill_hue(c=30)
grid.arrange(p1,p2,p3,nrow=1,ncol=3)
Dải màu mặc định đối với biến rời rạc sử dụng tham số \(c\) bằng 100 và tham số \(l\) bằng 65 trong khi tham số \(h\) nhận các giá trị cách đều nhau, bắt đầu từ \(h = 15 (độ)\). Lưu ý rằng \(h\) nhận giá trị từ 0 độ đến 360 độ nên trong trường hợp biến rời rạc có năm giá trị, các màu sắc sẽ lần lượt nhận các giá trị h = 15, 15 + 360/5, 15 + 2 * 360/5, 15 + 3 * 360/5 và 15 + 4 * 360/5. Đó là màu sắc của các thanh trong đồ thị barplot bên tay trái theo thứ tự từ trái qua phải. Trong hình ở giữa, khi chúng ta sử dụng giá trị tịnh tiến giá trị h lên 360/5, chúng ta có thể thấy các màu sắc bắt đầu từ h = 15 + 360/5 và kết thúc ở h = 15. Trong hình bên phải, chúng tôi giảm độ chói (tham số \(c\)) xuống còn 40. Chúng ta có thể thấy dải màu vẫn tương tự như hình ban đầu nhưng không đạt được độ sáng như vậy.
Bạn đọc cũng có thể sử dụng các dải màu rời rạc được thiết kế sẵn cho mục đích trực quan hóa các biến rời rạc. Dải màu rời rạc mà chúng tôi thường sử dụng là dải màu Brewer. Những dải màu này được thiết kế để hoạt động tốt trong nhiều tình huống khác nhau kể cả đối với những người khó khăn khi nhận biết màu sắc hay khi sử dụng để hiển thị trên những bề mặt lớn. Hàm số để kiểm soát ánh xạ thẩm mỹ màu sắc sử dụng dải màu Brewer là scale_color_brewer() và scale_fill_brewer(). Bạn đọc cần sử dụng thư viện \(RColorBrewer\) để gọi được các hàm này. Để xem các dải màu có sẵn trong thư viện này, bạn đọc sử dụng câu lệnh sau
display.brewer.all()Bạn đọc sử dụng tùy biến \(palette\) trong hàm scale_color_brewer() để lựa chọn dải màu có sẵn.
p1<-gapminder%>%filter(year==2011)%>%
ggplot(aes(continent,fill=continent))+geom_bar()+
scale_fill_brewer(palette = "Dark2")+
ggtitle("Sử dụng dải màu Dark2")
p2<-gapminder%>%filter(year==2011)%>%
ggplot(aes(continent,fill=continent))+geom_bar()+
scale_fill_brewer(palette = "Set1")+
ggtitle("Sử dụng dải màu Set1")
p3<-gapminder%>%filter(year==2011)%>%
ggplot(aes(continent,fill=continent))+geom_bar()+
scale_fill_brewer(palette = "Spectral")+
ggtitle("Sử dụng dải màu Spectral")
grid.arrange(p1,p2,p3,nrow=1,ncol=3)
Để tạo ra dải màu rời rạc theo ý muốn của mình, bạn đọc sử dụng hàm scale_fill_manual() và scale_color_manual(). Tham số \(values\) trong hàm này nhận giá trị là véc-tơ chứa màu sắc mà bạn đọc tự tạo. Lưu ý rằng số lượng phần tử trong véc-tơ phải tương ứng với số lượng phần tử trong biến rời rạc.
Một hàm số có thể được sử dụng để nội suy ra các màu sắc “cách đều nhau” trong không gian màu “RGB” hoặc không gian màu Lab là hàm số colorRampPalette() của thư viện \(grDevices\). Để tạo ra một véc-tơ có độ dài 5, mỗi giá trị là một màu sắc được nội suy tuyến tính từ màu xanh lam đến màu đỏ chúng ta sử dụng colorRampPalette() như sau
# nội suy trong RGB
mypalette1<-colorRampPalette(c("blue","red"), space = "rgb")(5)
# nội suy trong Lab
mypalette2<-colorRampPalette(c("blue","red"), space = "Lab")(5)Các đồ thị barplot dưới đây sử dụng các màu sắc mà chúng ta tự chỉ định bằng hàm scale_fill_manual()
p1<-gapminder%>%filter(year==2011)%>%
ggplot(aes(continent,fill=continent))+geom_bar()+
scale_fill_manual(values = c("blue","green","grey","yellow","red"))+
ggtitle("Màu tự định nghĩa")
p2<-gapminder%>%filter(year==2011)%>%
ggplot(aes(continent,fill=continent))+geom_bar()+
scale_fill_manual(values = mypalette1)+
ggtitle("Màu nội suy trong RGB")
p3<-gapminder%>%filter(year==2011)%>%
ggplot(aes(continent,fill=continent))+geom_bar()+
scale_fill_manual(values = mypalette2)+
ggtitle("Màu nội suy trong Lab")
grid.arrange(p1,p2,p3,nrow=1,ncol=3)
Cách sử dụng tham số \(limits\), \(breaks\), và \(label\) cũng gần tương tự như đối với biến liên tục. Tham số \(limits\) cho biết các giá trị nào trong biến rời rạc được ánh xạ tới dải màu sắc. Tham số \(breaks\) cho biết các giá trị nào không được sử dụng trong ánh xạ thẩm mỹ. \(label\) cho biết cách các màu sắc hiển thị trong phần chú giải. Theo kinh nghiệm của chúng tôi thì tham số \(breaks\) không có nhiều ý nghĩa khi sử dụng đối với dải màu sắc liên tục, trong khi tham số \(limits\) có ý nghĩa quan trọng khi bạn đọc cần cố định ánh xạ màu sắc lên biến rời rạc khi vẽ nhiều biểu đồ khác nhau và để kiểm soát thứ tự xuất hiện của biến liên tục trên đồ thị.
p1<-gapminder%>%filter(year==1960, continent == "Asia")%>%
arrange(-population)%>%head(10)%>%
ggplot(aes(fill=country))+
geom_bar(aes(x = population, y = reorder(country,population)),stat="identity",col="black")+
ylab("")+ggtitle("Năm 1960")+
scale_x_continuous(labels = scales::comma)+
scale_fill_manual(values = c("blue","red","yellow"), limits = c("Philippines","Vietnam", "Indonesia"))
p2<-gapminder%>%filter(year==2010, continent == "Asia")%>%
arrange(-population)%>%head(10)%>%
ggplot(aes(fill=country))+
geom_bar(aes(x = population, y = reorder(country,population)),stat="identity",col="black")+
ylab("")+ggtitle("Năm 2010")+
scale_x_continuous(labels = scales::comma)+
scale_fill_manual(values = c("blue","red","yellow"), limits = c("Philippines","Vietnam", "Indonesia"))
grid.arrange(p1,p2,nrow=1,ncol=2)
Sử dụng \(limits\) trong đồ thị ở trên giúp chúng ta nhấn mạnh vào 3 quốc gia Philippines, Vietnam, và Indonesia trong nhóm 10 nước có dân số lớn nhất châu Á trong các năm 1960 và 2010.
8.4.3 Các thuộc tính thẩm mỹ khác
Ngoài vị trí và màu sắc, còn có một số thuộc tính thẩm mỹ khác mà \(ggplot2\) có thể sử dụng để mô tả dữ liệu. Trong phần này, chúng ta sẽ xem xét thuộc tính kích thước (size), hình dạng (shape), chiều rộng của line và kiểu line, sử dụng cùng với các thuộc tính vị trí và màu sắc để thể hiện tốt nhất các biến trong dữ liệu. Ngoài đề cập đến các giá trị mặc định, chúng tôi cũng sẽ thảo luận về các hàm số để bạn đọc có thể sử dụng để kiểm soát tốt các thuộc tính này.
8.4.3.1 Kích thước (size)
Thuộc tính thẩm mỹ kích thước thường được sử dụng để mô tả hình dạng đồ họa kiểu điểm hoặc ký tự. Như chúng tôi đã đề cập trong phần giới thiệu, thuộc tính kích thước thường được sử dụng với biến liên tục. Nếu không có hàm kiểm soát ánh xạ thẩm mỹ, bán kính của điểm tương ứng với giá trị nhỏ nhất luôn là 1 và bán kính của điểm có giá trị lớn nhất luôn là 6, nghĩa là có bán kính gấp 6 lần bán kính của điểm nhỏ nhất. Khi nội suy ra kích thước của các điểm khác, \(ggplot2\) mặc định cho kích thước của điểm là diện tích của hình tròn mô tả điểm đó chứ không phải đường kính của hình tròn. Kích thước của điểm sẽ phụ thuộc vào thứ hạng (rank) của giá trị đó trong biến liên tục chứ không được tính bằng giá trị thực của điểm đó. Nếu \(r_m\) là bán kính của hình tròn tương ứng với giá trị nhỏ nhất và \(r_M\) tương ứng với diện tích của hình tròn tương ứng với giá trị lớn nhất thì diện tích của hình tròn tương ứng với giá trị có thứ hạng \(k\) trong tổng số \(n\) giá trị của biến liên tục là \[\begin{align} area = r_m + (k-1) \times \cfrac{r_M - r_m}{n - 1} \end{align}\]
Bạn đọc có thể quan sát kích thước của các hình tròn trong hình dưới đây
dat<-data.frame(x=1:3,y=1:3,z=1:3)
# Hình bên trái
p1<-dat%>%ggplot(aes(x,y,size=z))+geom_point(shape=21,fill= "lightskyblue")+
theme(legend.position = "none")
# Hình ở giữa
p2<-dat%>%ggplot(aes(x,y,size=z^2))+geom_point(shape=21,fill= "lightskyblue")+
theme(legend.position = "none")
# Hình bên phải
p3<-dat%>%filter(z>=2)%>%ggplot(aes(x,y,size=z))+geom_point(shape=21,fill= "lightskyblue")+
theme(legend.position = "none")
grid.arrange(p1,p2,p3,ncol=3,nrow=1)
Hình bên tay trái: diện tích của hình tròn nằm ở tọa độ (2,2) bằng trung bình cộng diện tích của hình tròn nằm ở vị trí (1,1) và (3,3). Do diện tích của hình tròn nằm ở vị trí (3,3) bằng \(6^2 = 36\) lần diện tích của hình tròn tại vị trí \((1,1)\) nên diện tích của hình tròn tại (2,2) bằng \(\cfrac{36+1}{2} = 18,5 \textit{(lần)}\) diện tích hình tròn tại (1,1), hay nói cách khác đường kính của hình tròn tại vị trí (2,2) bằng \(\sqrt{18,5} \sim 4,3 \textit{ (lần)}\) đường kính của hình tròn tại vị trí (1,1). Hình ở giữa: cho thấy khi chúng ta ánh xạ thuộc tính thẩm mỹ vào \(z^2\) thay vì \(z\) thì kích thước các hình tròn vẫn không hề thay đổi do thứ hạng của các điểm trong véc-tơ \(z\) không thay đổi. Hình bên phải: khi chúng ta chỉ vẽ hai điểm thay vì cả ba điểm, diện tích hình tròn nhỏ nhất và hình tròn lớn nhất vẫn không thay đổi.
Hàm số dùng để kiểm soát giá trị của ánh xạ thẩm mỹ kích thước là hàm scale_size(). Để thay đổi miền giá trị của kích thước, chúng ta sử dụng tham số \(range\).
dat<-data.frame(x=1:3,y=1:3,z=1:3)
# Hình bên trái
p1<-dat%>%ggplot(aes(x,y,size=z))+geom_point(shape=21,fill= "lightskyblue")+
theme(legend.position = "none")
# Hình ở giữa
p2<-dat%>%ggplot(aes(x,y,size=z))+geom_point(shape=21,fill= "lightskyblue")+
scale_size(range=c(1,12))+
theme(legend.position = "none")
# Hình bên phải
p3<-dat%>%ggplot(aes(x,y,size=z))+geom_point(shape=21,fill= "lightskyblue")+
scale_size(range=c(6,24))+
theme(legend.position = "none")
grid.arrange(p1,p2,p3,ncol=3,nrow=1)
Hình bên trái: đường kính của hình nhỏ nhất là 1, của hình lớn nhất là 6. Hình ở giữa: đường kính của hình nhỏ nhất là 1, của hình lớn nhất là 12. Hình bên phải: đường kính của hình nhỏ nhất là 6, của hình lớn nhất là 24. Đường kính của các hình nằm ở giữa được nội suy tuyến tính theo diện tích tăng dần theo hạng của điểm đó.
Trong trường hợp bạn đọc muốn sử dụng nội suy tuyển tính theo đường kính của điểm thay vì diện tích, hãy sử dụng hàm scale_radius()
dat<-data.frame(x=1:3,y=1:3,z=1:3)
# Hình bên trái
p1<-dat%>%ggplot(aes(x,y,size=z))+geom_point(shape=21,fill= "lightskyblue")+
theme(legend.position = "none")+
scale_size(range=c(1,7))
# Hình ở giữa
p2<-dat%>%ggplot(aes(x,y,size=z))+geom_point(shape=21,fill= "lightskyblue")+
scale_radius(range=c(1,7))+
theme(legend.position = "none")
# Hình bên phải
p3<-dat%>%ggplot(aes(x,y,size=z))+geom_point(shape=21,fill= "lightskyblue")+
scale_radius(range=c(4,10))+
theme(legend.position = "none")
grid.arrange(p1,p2,p3,ncol=3,nrow=1)
Hình bên trái sử dụng scale theo diện tích và đường kính hình tròn lớn nhất bằng 7 lần đường tròn nhỏ; hình tròn ở giữa có bán kính bằng \(\sqrt{\cfrac{7^2+1^2}{2}} = 5 \textit{ (lần)}\) diện tích hình tròn nhỏ nhất. Hình ở giữa, do scale theo đường kính hình tròn nên hình ở giữa có đường kính bằng \(\cfrac{7+1}{2} = 4 \textit{ (lần)}\) đường kính hình tròn nhỏ. Hình bên phải: đường kính của hình nhỏ nhất là 4, hình lớn nhất là 10, nên bán kính hình ở giữa là \(\cfrac{4+10}{2} = 7\) (bằng kích thước của hình tròn lớn nhất của hình ở giữa).
Các tham số \(limits\), \(breaks\), và \(label\) được sử dụng tương tự như thuộc tính thẩm mỹ màu sắc. \(limits\) cho biết miền giá trị nào của biến được ánh xạ đến thuộc tính thẩm mỹ size. \(breaks\) cho biết các kích thước nào xuất hiện trên chú giải, và \(labels\) mô tả thuộc tính thẩm trên chú giải của đồ thị.
gapminder%>%filter(year==2010)%>%
ggplot(aes(infant_mortality,life_expectancy, size = population))+
geom_point(shape = 21, fill = "lightskyblue",alpha = 0.5)+
scale_size(range = c(1,12),
limits = c(10^7,max(gapminder$population)),
breaks = c(10^8,2*10^8,5*10^8,10^9),
#label = paste("Dân số",c(10^8,2*10^8,5*10^8,10^9)),
labels = scales::label_comma())
8.4.3.2 Hình dạng (shape)
Hình dạng thường được sử dụng để mô tả một biến rời rạc có không quá nhiều giá trị riêng biệt. Theo kinh nghiệm của tác giả thì hình dạng chỉ nên sử dụng với các biến có nhỏ hơn hoặc bằng 5 giá trị riêng biệt. Mặc dù \(ggplot2\) cho phép sử dụng lên đến hơn 20 hình dạng khác nhau nhưng sử dụng nhiều hơn 5 hình dạng trong một đồ thị sẽ làm cho đồ thị trở nên rắc rối và khó khăn trong nhận diện. Tại phiên bản \(ggplot2\) mà tác giả đang sử dụng, có 25 hình dạng khác nhau có thể dùng để mô tả biến rời rạc ứng với 25 số tự nhiên từ 1 đến 25 như sau
dat<-data.frame(x=c(rep(1:10,2),1:5),y = c(rep(3,10),rep(2,10),rep(1,5)), z = 1:25)
dat%>%ggplot(aes(x,y,shape=as.factor(z)))+geom_point(size=3)+
scale_shape_manual(values = 1:25)+theme_classic()+geom_text(aes(label=z),vjust=-1)+
theme(legend.position = "none")+
theme_void()
Bạn đọc lưu ý rằng có một số hình dạng trông giống nhau nhưng lại có thuộc tính thẩm mỹ khác nhau. Chẳng hạn như hình dạng tương ứng với số 1 là một điểm hình tròn với thuộc tính thẩm mỹ color là màu sắc của hình tròn đó, trong khi hình dạng tương ứng với số 21 có thuộc tính thẩm mỹ color là màu viền bên ngoài của hình tròn và thuộc tính thẩm mỹ fill mới là màu sắc bên trong hình tròn.
Để kiểm soát ánh xạ thẩm mỹ đến thuộc tính hình dạng, bạn đọc sử dụng hàm scale_shape_manual().
gapminder%>%filter(year==2010, continent == "Asia")%>%
ggplot(aes(gdp/population,life_expectancy, shape = region))+
geom_point()+
scale_x_continuous(trans="log10")+
scale_shape_manual(values=c(21:24,8) )
Nhìn chung ánh xạ biến rời rạc đến thuộc tính thẩm mỹ hình dạng không cho hiệu quả tốt trong phân biệt các nhóm. Bạn đọc nên thận trọng khi sử dụng thuộc tính thẩm mỹ này.
8.4.3.3 Kích thước và hình dạng của các đường (linewidth và linetype)
Đối với hình dạng đồ họa kiểu các đường như geom_line(), geom_path(), geom_segment() chúng ta có thể ánh xạ các biến rời rạc vào độ rộng hoặc hình dạng của đường. Hình vẽ dưới đây mô tả sự thay đổi của biến \(gdp\) của ba quốc gia bao gồm Mỹ, Trung Quốc và Nhật Bản theo thời gian từ năm 1960 đến năm 2010.
gapminder%>%filter(country %in% c("United States", "China", "Japan"), year <= 2011)%>%
ggplot(aes(x = year,y = gdp/10^9))+
geom_line(aes(linetype = country))+
theme_minimal()+
ylab("GDP in $B")+
scale_x_continuous(breaks = seq(1960,2010,10))+
scale_y_continuous(labels = scales::label_comma())
Hàm số dùng để kiểm soát ánh xạ thẩm mỹ vào hình dạng của đường là scale_linetype_manual(). \(ggplot2\) có 13 hình dạng cho các đường được đánh số từ 1 đến 13 như dưới đây

Để các đường có hình dạng như mong muốn, chúng ta gán giá trị tham số \(values\) cho véc-tơ chứa các số từ 1 đến 13 là hình dạng mà bạn lựa chọn.
gapminder%>%filter(country %in% c("United States", "China", "Japan"), year <= 2011)%>%
ggplot(aes(x = year,y = gdp/10^9))+
geom_line(aes(linetype = country))+
theme_minimal()+
ylab("GDP in $B")+
scale_x_continuous(breaks = seq(1960,2010,10))+
scale_y_continuous(labels = scales::label_comma())+
scale_linetype_manual(values = c(4,7,12))
8.5 Chú giải của ánh xạ thẩm mỹ
Về mặt hình thức, nếu coi các hàm scale_*() trong phần trước của chương sách như các ánh xạ từ một tập hợp các giá trị của biến đến một tập hợp các giá trị của thuộc tình thẩm mỹ thì chú giải là ánh xạ ngược từ thuộc tính thẩm mỹ đến miền giá trị của biến. Chú giải cho phép bạn chuyển đổi các thuộc tính trực quan trở lại giá trị của dữ liệu. Giá trị xuất hiện trên các trục tọa độ và các chú giải có cách hiển thị khác nhau nhưng về bản chất lại có cùng một mục đích là cho phép người tiếp nhận quan sát các hình ảnh đồ họa trực quan và ánh xạ chúng trở lại giá trị của dữ liệu.
Chú giải có khả năng giải thích tốt hơn giá trị xuất hiện trên các trục tọa độ bởi các nguyên nhân sau
Chú giải có thể giải thích nhiều biến cùng lúc trong khi giá trị trên trục tọa độ chỉ cho phép một biến.
Chú giải có thể tùy biến dễ hơn: có thể xuất hiện ở các vị trí theo ý muốn của người xây dựng đồ thị, có thể xuất hiện theo bất kỳ hướng nào.
Bạn đọc hãy lưu ý rằng dù chúng ta không gọi bất kỳ hàm scale_*() nào trong các câu lệnh thì \(ggplot2\) vẫn luôn luôn sử dụng một hàm \(scale\) mặc định để ánh xạ từ biến đến miền giá trị của thuộc tính thẩm mỹ. Mỗi khi bạn gọi hàm scale để kiểm soát ánh xạ thẩm mỹ, các giá trị mà bạn khai báo sẽ thay thế cho các giá trị mặc định. Trong trường hợp bạn gọi nhiều hàm scale tác động đến một thuộc tính thẩm mỹ thì chỉ có hàm scale_*() sau cùng bạn gọi ra sau cùng được sử dụng.
p<-gapminder%>%filter(year==2010,region=="South-Eastern Asia")%>%
mutate(gdp_per_capita = gdp/population)%>%
ggplot(aes(reorder(country,gdp_per_capita),gdp_per_capita,fill = country))+
geom_bar(stat="identity")+
theme_minimal()
p+scale_y_continuous(name = "GDP bình quân đầu người", labels = scales::label_comma())+
scale_x_discrete(name = "Country")+
scale_y_continuous(trans = "sqrt")+
scale_x_discrete(name = "Quốc gia", labels = c(
"Vietnam" = "VN",
"Thailand" = "TL",
"Timor-Leste" = "Đông Timor"))+
scale_y_continuous(name = "GDP bình quân đầu người", labels = scales::label_dollar())
Khi bạn sử dụng nhiều hàm scale_*() tác động đến cùng một thuộc tính thẩm mỹ như trên, \(ggplot2\) sẽ đưa ra các cảnh báo. Bạn cần xem xét lại các câu lệnh của mình để đảm bảo sử dụng đúng với mục đích.
Nhìn chung để kiểm soát chú giải của các ánh xạ thẩm mỹ, bạn đọc sử dụng tham số \(guide\) trong các hàm scale_*() tương ứng. Giá trị gán cho tham số \(guide\) là một trong các hàm số sau đây:
-
guide_axis()là hàm số dùng để gán cho tham số \(guide\) khi chúng ta sử dụng các hàmscale_*()nhằm kiểm soát ánh xạ thẩm mỹ đến các trục tọa độ.
p+ scale_x_discrete(name = "Quốc gia",
guide = guide_axis(title = "Country",
angle = 90))+
scale_y_continuous(name = "GDP bình quân đầu người", labels = scales::label_dollar(),
guide = guide_axis(title = "GDP per capita"))
Bạn đọc có thể thấy rằng tham số \(title\) trong hàm \(guide\) đã thay thế cho tham số \(name\) trong hàm scale_(). Tham số \(angle\) cho biết hướng các giá trị xuất hiện trên trục tọa độ. Bạn đọc tham khảo hướng dẫn sử dụng hàm guide_axis() để hiểu về các tham số khác như \(n.dodge\), \(order\), hay \(position\).
-
guide_legend()là hàm số dùng để gán cho tham số \(guide\) khi gọi các hàmscale_*()kiểm soát ánh xạ từ các biến rời rạc đến màu sắc. Có rất nhiều tham số có thể sử dụng trong hàm số này. Bạn đọc tham khảo hướng dẫn sử dụng hàm để biết đầy đủ các tham số.
p+scale_x_discrete(guide = guide_axis(title = "Quốc gia",
angle = 90))+
scale_y_continuous(labels = scales::label_dollar(),
guide = guide_axis(title = "GDP per capita"))+
scale_fill_brewer(palette = "Paired",
guide = guide_legend(
title = "Quốc gia",
title.position = "top",
ncol = 2
))
guide_colorbar()được dùng khi chú giải cho các ánh xạ từ biến liên tục đến dải màu liên tụcguide_bin()được dùng khi chú giải cho các ánh xạ từ biến liên tục đến thuộc tính thẩm mỹ kích thước (size).
8.7 Chủ đề và ngữ cảnh của đồ thị (theme)
Trong phần này, chúng ta sẽ học cách sử dụng chủ đề và ngữ cảnh (theme) cho các đồ thị. Ngữ cảnh cho phép bạn đọc kiểm soát tốt các cấu phần không ánh xạ đến dữ liệu trong câu chuyện của bạn. Nhìn chung chủ đề và ngữ cảnh không ảnh hưởng đến cách dữ liệu được hiển thị bằng các hình dạng đồ họa hoặc cách dữ liệu được biến đổi. Chủ đề và ngữ cảnh cho phép bạn đọc kiểm soát những cấu phần như phông chữ, hình nền, vị trí chú giải,…
Sự phân tách giữa các thành các phần có ánh xạ đến dữ liệu và thành phần không ánh xạ đến dữ liệu trong \(ggplot2\) là điểm khác biệt so với đồ họa cơ sở. Trong đồ họa cơ sở hầu hết các hàm đều có một số lượng lớn các tham số số chỉ định cả hình thức dữ liệu và phần không liên quan đến dữ liệu, điều này làm cho các hàm trong đồ thị cơ sở trở nên phức tạp. \(ggplot2\) tiếp cận theo cách khác: khi tạo đồ thị, bạn xác định cách hiển thị dữ liệu trước, sau đó bạn có thể chỉnh sửa mọi chi tiết không liên quan đến dữ liệu bằng cách hàm kiểm soát chủ đề và ngữ cảnh. Để kiểm soát chủ đề và ngữ cảnh của đồ thị, bạn đọc cần nắm vững các nội dung sau:
Các chủ đề và ngữ cảnh đã được hoàn chỉnh sẵn có trong \(ggplot2\) và trong thư viện \(ggthemes\).
Kiểm soát các thành phần của chủ đề và ngữ cảnh như: tiêu đề của đồ thị (kiểu chữ, kích thước, vị trí), cách hiển thị các số trên các trục, cách hiển thị các hình dạng đồ họa trên chú giải, kiểu chữ, kích thước hay vị trí của chú giải…
Kiểm soát các tùy biến của các hàm dùng để gán giá trị cho các thành phần của chủ đề. Ví dụ như hàm
element_text()có thể dùng để chỉnh kích thước phông chữ, màu sắc và giao diện của các thành phần văn bản.Cách sử dụng hàm
theme()với một danh sách dài các tùy biến cho phép bạn ghi đè lên các thành phần của chủ đề và ngữ cảnh mặc định.
8.8 Tạo đồ thị tương tác và đồ thị động.
Các đồ thị của thư viện \(ggplot2\) đều là các đồ thị tĩnh. Các đồ thị động hay đồ thị tương tác ngoài lợi thế hơn đồ thị tĩnh ở việc thu hút thị giác của người tiếp nhận còn ở khả năng mô tả dữ liệu một cách đầy đủ thông tin hơn:
Các đồ thị dạng động đặc biệt hiệu quả trong việc mô tả sự thay đổi dữ liệu theo thời gian.
Các đồ thị tương tác cho phép hiển thị thông tin bằng con trỏ, hoặc phóng to, thu nhỏ từng phần của đồ thị. Bạn đọc tránh phải hiển thị quá nhiều thông tin lên đồ thị cùng lúc.
Khuyết điểm duy nhất của các đồ thị tương tác và các đồ thị động đó là không thể biểu diễn trên các bản in cứng.
Trong phần này của chương sách, chúng tôi sẽ thảo luận về hai thư viện dùng để tạo đồ thị tương tác và đồ thị dạng động sử dụng cùng với \(ggplot2\) là \(ggiraph\) và \(plotly\). Nếu như \(ggiraph\) là thư viện bổ sung cho \(ggplot2\) và được xây dựng dựa trên cấu trúc ngữ pháp đồ thị tương tự như \(ggplot2\) thì \(plotly\) là một thư viện khá độc lập với \(ggplot2\) và chuyên sử dụng để tạo đồ thị dạng động và tương tác.
8.8.1 Tạo đồ thị tương tác với \(ggiraph\)
Ưu điểm lớn nhất của \(ggiraph\) đó là các câu lệnh tạo đồ thị cũng được dựa trên ngữ pháp của đồ thị, nghĩa là hoàn toàn tương đồng với các câu lệnh trong \(ggplot2\). Để tạo một đồ thị trong \(ggiraph\), bạn đọc chỉ cần thêm các thuộc tính thẩm mỹ của đồ thị tương tác và đồ thị động cùng với các thuộc tính của đồ thị tĩnh của \(ggplot2\). Tại thời điểm chúng tôi viết chương sách này, thư viện \(ggiraph\) đang ở phiên bản 0.8.7 và hướng dẫn sử dụng ở trong link dưới đây
https://cloud.r-project.org/web/packages/ggiraph/ggiraph.pdf
Sau khi xem qua danh sách các hàm số trong thư viện \(ggiraph\), bạn đọc có thể thấy rằng đa số các hàm geom_*() trong thư viện \(ggplot2\) đều có một hàm tương ứng để tạo đồ thị tương tác là geom_*_interactive(). Chẳng hạn như hàm geom_point() trong thư viện \(ggplot2\) sẽ có hàm tương ứng trong \(ggiraph\) là geom_point_interactive(). Hai cấu phần thẩm mỹ cho đồ thị tương tác là \(tooltip\) và \(data_id\). Cũng giống như \(plotly\), bạn đọc cần tạo một đối tượng kiểu đồ thị bằng hàm ggplot() sau đó sử dụng hàm girafe() để tạo đồ thị tương tác. Hãy quan sát ví dụ dưới đây:
p<-murders %>% ggplot(aes(y = total, x = population)) +
geom_point_interactive(aes(fill=region,
tooltip = paste0("State: ", state, "\n Region: ", region, "\n Population: ", population),
onclick = region),
size = 4, shape=21, alpha = 0.8, color = "black") +
geom_smooth(method = "lm", se = FALSE, linetype = 2, color="grey")+
scale_x_continuous(trans = "log10", labels = scales::label_comma()) +
scale_y_log10() +
scale_fill_brewer(palette = "Dark2")+
theme_minimal()+
ggtitle("Số vụ sát nhân bằng súng tại các bang năm 2010")
girafe(ggobj = p)Thuộc tính thẩm mỹ \(tooltip\) cho biết thông tin hiển thị của các điểm trên đồ thị khi sử dụng con trỏ trong khi thuộc tính thẩm mỹ \(data\_id\) khi được ánh xạ đến từ một biến sẽ cho biết các quan sát có cùng giá trị trên biến đó. Bạn đọc có thể sử dụng con trỏ di chuyển đến từng các điểm để xem kết quả của ánh xạ đến thuộc tính \(tooltip\) và \(data_id\).
Cách sử dụng các thuộc tính thẩm mỹ \(tooltip\) và \(data\_id\) hoàn toàn tương tự trong các đồ thị cơ bản khác.
- Đồ thị dạng bong bóng
p<-diamonds%>%group_by(cut,color)%>%mutate(ave_price = mean(price))%>%ungroup()%>%
as.data.frame()%>%
ggplot(aes(cut,color,color = ave_price))+
geom_count_interactive(aes(tooltip = paste0("Number: ", after_stat(n))))+
scale_color_continuous(type = "viridis")+
scale_size(range=c(1,12))+
theme_minimal()
girafe(ggobj = p)- Đồ thị dạng line: Đồ thị dưới đây mô tả tỷ lệ thất nghiệp của nước Mỹ qua các thời kỳ Tổng thống và các Đảng
dat1<-presidential[3:11,]
p<-economics%>%mutate(unemploy_rate = unemploy/pop)%>%
ggplot()+
geom_line_interactive(aes(x=date,y=unemploy_rate,tooltip = name, data_id = party))+
scale_y_continuous(limits = c(0.013,0.052))+
geom_rect(data=dat1,
aes(xmin = start, xmax = end,
ymin = 0.013, ymax = 0.052,fill = party),alpha = 0.4)+
geom_rect_interactive(data=dat1,
aes(xmin = start, xmax = end,
ymin = 0.013, ymax = 0.052,
tooltip = name,
data_id = name),color = "black",size=0.1,alpha=0.01)+
scale_fill_manual(values=c("blue","red"))+
theme_minimal()
girafe(ggobj = p)Đồ thị dạng thanh
Bản đồ tương tác
Chúng ta sẽ mô tả biến infant_mortality của dữ liệu \(gapminder\) thông qua bản đồ thế giới
8.8.2 Tạo đồ thị tương tác với \(plotly\)
Để tạo một đồ thị tương tác bằng thư viện \(plotly\) dễ hơn bạn nghĩ rất nhiều. Việc duy nhất bạn đọc cần làm là gọi thư viện \(plotly\) và sau đó sử dụng hàm ggplotly(). Đoạn câu lệnh dưới đây mô tả dữ liệu \(murders\) dưới dạng đồ thị rải điểm tương tác.
p<-murders %>% ggplot(aes(y = total, label = state, x = population)) +
geom_point(aes(fill=region), size = 4, shape=21, alpha = 0.8, color = "black") +
geom_smooth(method = "lm", se = FALSE, linetype = 2, color="grey")+
scale_x_continuous(trans = "log10", labels = scales::label_comma()) +
scale_y_log10() +
scale_fill_brewer(palette = "Dark2")+
theme_minimal()+
ggtitle("Số vụ sát nhân bằng súng tại các bang năm 2010")
ggplotly(p)Bạn đọc có thể tương tác với đồ thị bằng các thao tác như sau:
Sử dụng con trỏ chỉ vào các điểm để xem thông tin chính xác về dân số, số vụ sát nhân, tên bang, và tên vùng của mỗi điểm.
Sử dụng con trỏ, hoặc các nút phóng to, thu nhỏ để xem từng phần của đồ thị.
Sử dụng con trỏ trên chú giải để lựa chọn các vùng nào hiển thị, hoặc không hiển thị trên đồ thị.
Sử dụng con trỏ trượt theo đường thẳng tạo bởi
geom_smooth()để biết giá trị trên trục total và population của mỗi điểm trên đường thẳng. Lưu ý rằng giá trị xuất hiện là giá trị sau khi đã chuyển đổi bằng hàm \(log10()\).
Các thông tin bạn đọc muốn hiển thị bằng con trỏ là các dữ liệu đã được ánh xạ vào trong các thuộc tính thẩm mỹ của đồ thị. Trong đồ thị ở trên thông tin của mỗi điểm bao gồm có 1: Population, 2: Total, 3: State, và 4: region. Nếu không sử dụng \(plotly\), thuộc tính thẩm mỹ \(label\) sẽ không được hiển thị do chúng ta không sử dụng geom_text() hay geom_label(). \(plotly\) sẽ hiển thị thông tin của tất cả các biến có ánh xạ đến thuộc tính thẩm mỹ, dù thuộc tính thẩm mỹ đó không hiển thị trong \(ggplot2\).
Tham số \(tooltip\) trong hàm \(ggplotly\) được sử dụng để kiểm soát các thuộc tính thẩm mỹ xuất hiện trên đồ thị tương tác. Ví dụ như trong đồ thị rải điểm ở trên, bạn đọc không chỉ muốn thông tin hiển thị trên mỗi điểm chỉ bao gồm tên bang (thuộc tính thẩm mỹ \(fill\)) và vùng (thuộc tính thẩm mỹ \(label\)), chúng ta sử dụng tùy biến \(tooltip\) như sau
# Thông tin mỗi điểm chỉ bao gồm 1. Tên bang (label) và 2. Vùng (fill)
ggplotly(p, tooltip = c("label","fill"))Chúng ta có thể sử dụng \(ggplotly\) trên hầu hết các đồ thị được tạo bởi \(ggplot2\), dưới đây là một số ví dụ
- Đồ thị kiểu bong bóng:
p<-diamonds%>%group_by(cut,color)%>%mutate(ave_price = mean(price))%>%ungroup()%>%
as.data.frame()%>%
ggplot(aes(cut,color,color = ave_price))+
geom_count()+
scale_color_continuous(type = "viridis")+
scale_size(range=c(1,12))+
theme_minimal()
ggplotly(p, tooltip = c("n", "fill"))- Đồ thị kiểu line
p<-gapminder%>%filter(country %in% c("United States","Japan","China","Germany","France"),
year <= 2011)%>%mutate(gdp_bil_usd = gdp/10^9)%>%
ggplot(aes(x = year, y = gdp_bil_usd, color = country, linetype = country))+
geom_line(size=0.5)+
scale_y_continuous(labels = scales::label_comma())+
theme_minimal()
ggplotly(p, tooltip = c("x","y", "color") )- Đồ thị kiểu thanh: barplot kết hợp với thuộc tính thẩm mỹ \(fill\) có thể sử dụng để trực quan hóa hai biến rời rạc. Đồ thị dưới đây mô tả thu nhập bình quân đầu người tại các Châu lục vào năm 2010 theo các mức độ: dưới $2000, từ $2000 đến $5000, và trên $5000.
p<-gapminder%>%filter(year == 2010)%>%drop_na()%>%
mutate(gdp_per_capita = gdp/population,
gdp_levels = ifelse(gdp_per_capita<2000,"Low",
ifelse(gdp_per_capita<5000,"Medium","High")),
gdp_range = factor(gdp_levels, levels = c("High","Medium","Low")))%>%
ggplot(aes(x = continent,fill = gdp_range))+
geom_bar(color="grey",alpha=0.6)+
scale_fill_manual(values = c("green","blue","red"))+
theme_minimal()
ggplotly(p, tooltip = "count")- Bản đồ tương tác: bản đồ tương tác giúp cho việc hiển thị dữ liệu trên bản đồ trở nên đơn giản hơn rất nhiều so với sử dụng
geom_text()hoặcgeom_label()
dat<-map_data("state")
dat1<-murders%>%mutate(murder_rate=total/population*10^6,
state = tolower(state))
p<-dat%>%mutate(state = region)%>%
mutate(murder_rate=dat1$murder_rate[match(state,dat1$state)])%>%
ggplot(aes(x=long,y=lat,group=group,label = state, fill=murder_rate))+
geom_polygon(color="black",size = 0.1)+
scale_x_continuous(expand=c(0,0))+
scale_fill_gradientn(colors = c(rgb(0.95,0.95,0.95),rgb(0.95,0.3,0.3),
rgb(0.95,0.1,0.1)))+
theme_minimal()+
theme(legend.position = "bottom")
ggplotly(p)Tạo đồ thị dạng động (dynamic) là một phương pháp thường được sử dụng để mô tả dữ liệu biến đổi theo thời gian. Đồ thị dạng động ngoài yếu tố bắt mắt còn giúp cho người tiếp nhận dữ liệu cảm nhận được bản chất của vấn đề phức tạp một cách trực quan nhất. Hãy bắt đầu với một dữ liệu đơn giản bao gồm hai biến \(x\), \(y\) và thời gian \(time\).
dat<-data.frame(x=1:10,y=1:10,time=1:10)Chúng ta muốn vẽ một điểm tại các vị trí lưu ở cột \(x\) và cột \(y\) di chuyển theo thời gian được lưu trong cột \(time\) với \(ggplotly\), chúng ta chỉ cần khai báo thêm ánh xạ thẩm mỹ từ thuộc tính \(frame\) của hàm animation_opts() đến biến \(time\) như dưới đây
p<-dat%>%ggplot(aes(x=x,y=y,size=time, frame = time, color= time))+
geom_point()+
theme_minimal()
ggplotly(p, width = 600, height = 600, tooltip = "color") %>%
animation_slider(frame = 1000)Đồ thị dạng động sẽ được kích hoạt mỗi khi chúng ta bấm nút “play”. Các tham số \(width\) và \(height\) trong hàm ggplotly() cho biết kích thước của đồ thị dạng động trong khi tham số \(frame\) trong hàm animation_opts() cho biết độ mượt của hình động.
Giả sử bạn đọc muốn theo dõi mối quan hệ giữa hai biến tuổi thọ trung bình và tỷ lệ sinh trung bình của tất cả các quốc gia trên thế giới. Bạn sử dụng đồ thị rải điểm để mô tả mối quan hệ giữa hai biến liên tục, sử dụng màu sắc để phân biệt giữa các châu lục, sử dụng kích thước của các điểm để mô tả dân số, và sau cùng bạn sử dụng \(frame\) để mô tả biến \(year\). Với một vài điều chỉnh ánh xạ thẩm mỹ, bạn đã có thể kể được một câu chuyện hấp dẫn về tuổi thọ trung bình và tỷ lệ sinh dựa trên dữ liệu \(gapminder\)
p<-gapminder%>%filter(year %in% 1960:2011)%>%
ggplot(aes(x = fertility, y = life_expectancy, size = population,
fill = continent, frame = year, label = country))+
geom_point(alpha = 0.5,shape=21)+
scale_fill_brewer(palette = "Set1")+
scale_size(range=c(1,15))+
theme_minimal()+
ggtitle("Tuổi thọ và tỷ lệ sinh trung bình 1960 đến 2011")
ggplotly(p, width = 800, height = 600, tooltip = c("label","size") ) %>%
animation_slider(frame = 204)Một trong những công việc khó khăn nhất của những người làm việc liên quan đến xây dựng các mô hình toán học phức tạp là giải thích kết quả của mình cho những người ít có kiến thức chuyên môn về lĩnh vực này. Kinh nghiệm của chúng tôi là hãy trực quan hóa kết quả của mình thay vì các công thức phức tạp. Dưới đây là một vài khái niệm toán học phức tạp được giải thích dưới dạng đồ thị động
Chuyển động Brown: chuyển động Brown là một quá trình ngẫu nhiên có ý nghĩa đặc biệt quan trọng trong tài chính, bảo hiểm, và cả các lĩnh vực công nghệ. Không dễ dàng để giải thích cho những người không có nền tảng về toán các khái niệm về chuyển động Brown. Thay vì các công thức toán, chúng ta có thể giải thích về chuyển động Brown thông qua trực quan hóa:
Markov Chain Monte Carlo là một kỹ thuật mô phỏng biến ngẫu nhiên hoặc một véc-tơ ngẫu nhiên có hàm phân phối \(F\) mà không thể mô phỏng được một cách trực tiếp. Quá trình tạo ra biến ngẫu nhiên có hàm phân phối \(F\) sẽ bắt đầu từ một phân phối \(G\) mà chúng ta có thể mô phỏng ra được đi qua các hàm phân phối trung gian và sẽ hội tụ đến phân phối \(F\). Hình vẽ dưới đây mô tả quá trình mô phỏng biến ngẫu nhiên phân phối chuẩn \(\mathcal{N}(0,1)\) từ một phân phối có hai đinh (2 mode).